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 e8fc756421904ad104a84672620807c17092f68a..0000000000000000000000000000000000000000
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/polls/HidePollResultReqMsgHdlr.scala
+++ /dev/null
@@ -1,31 +0,0 @@
-package org.bigbluebutton.core.apps.polls
-
-import org.bigbluebutton.common2.msgs._
-import org.bigbluebutton.core.models.Polls
-import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
-
-trait HidePollResultReqMsgHdlr {
-  this: MeetingActor =>
-
-  val outGW: OutMsgRouter
-
-  def handleHidePollResultReqMsg(msg: HidePollResultReqMsg): Unit = {
-
-    def broadcastEvent(msg: HidePollResultReqMsg, hiddenPollId: String): Unit = {
-      val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, props.meetingProp.intId, msg.header.userId)
-      val envelope = BbbCoreEnvelope(PollHideResultEvtMsg.NAME, routing)
-      val header = BbbClientMsgHeader(PollHideResultEvtMsg.NAME, props.meetingProp.intId, msg.header.userId)
-
-      val body = PollHideResultEvtMsgBody(msg.header.userId, hiddenPollId)
-      val event = PollHideResultEvtMsg(header, body)
-      val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
-      outGW.send(msgEvent)
-    }
-
-    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 2125eb817acd83b1aa7acdccdb987b5f9861bbf3..bb88c5914dfd58d11824a9c2b94e362df0fc59da 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
@@ -3,7 +3,6 @@ package org.bigbluebutton.core.apps.polls
 import org.bigbluebutton.core.running.{ MeetingActor }
 
 trait PollApp2x 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 996a2351c70b6bdffa18a67628ada8b6acba69cd..01f9ee7bd4f2a7799ec58820f831cf751d02caf0 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
@@ -13,22 +13,34 @@ trait RespondToPollReqMsgHdlr {
   def handleRespondToPollReqMsg(msg: RespondToPollReqMsg): 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, props.meetingProp.intId, msg.header.userId)
-      val envelope = BbbCoreEnvelope(UserRespondedToPollEvtMsg.NAME, routing)
-      val header = BbbClientMsgHeader(UserRespondedToPollEvtMsg.NAME, props.meetingProp.intId, msg.header.userId)
+      val envelope = BbbCoreEnvelope(PollUpdatedEvtMsg.NAME, routing)
+      val header = BbbClientMsgHeader(PollUpdatedEvtMsg.NAME, 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)
+      outGW.send(msgEvent)
+    }
+
+    def broadcastUserRespondedToPollRecordMsg(msg: RespondToPollReqMsg, pollId: String, answerId: Int): Unit = {
+      val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, props.meetingProp.intId, msg.header.userId)
+      val envelope = BbbCoreEnvelope(UserRespondedToPollRecordMsg.NAME, routing)
+      val header = BbbClientMsgHeader(UserRespondedToPollRecordMsg.NAME, props.meetingProp.intId, msg.header.userId)
+
+      val body = UserRespondedToPollRecordMsgBody(pollId, answerId)
+      val event = UserRespondedToPollRecordMsg(header, body)
       val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
       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/apps/users/MuteUserCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/MuteUserCmdMsgHdlr.scala
index 2c6673797b8c578b457ad98c95702d3cbab817e3..ccbe0920841fad270ee0a8c0da704b6ee5d8c90d 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/MuteUserCmdMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/MuteUserCmdMsgHdlr.scala
@@ -16,10 +16,12 @@ trait MuteUserCmdMsgHdlr {
     for {
       u <- VoiceUsers.findWithIntId(liveMeeting.voiceUsers, msg.body.userId)
     } yield {
-      log.info("Send mute user request. meetingId=" + props.meetingProp.intId + " userId=" + u.intId + " user=" + u)
-      val event = MsgBuilder.buildMuteUserInVoiceConfSysMsg(props.meetingProp.intId, props.voiceProp.voiceConf,
-        u.voiceUserId, !u.muted)
-      outGW.send(event)
+      if (u.muted != msg.body.mute) {
+        log.info("Send mute user request. meetingId=" + props.meetingProp.intId + " userId=" + u.intId + " user=" + u)
+        val event = MsgBuilder.buildMuteUserInVoiceConfSysMsg(props.meetingProp.intId, props.voiceProp.voiceConf,
+          u.voiceUserId, msg.body.mute)
+        outGW.send(event)
+      }
     }
   }
 }
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserJoinMeetingAfterReconnectReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserJoinMeetingAfterReconnectReqMsgHdlr.scala
index b53f1dcffee6cf060dc4967490a739eb71dea226..7d3c61bea73dfe3d7d2ebfb3dbdf443aa8cbcfeb 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserJoinMeetingAfterReconnectReqMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserJoinMeetingAfterReconnectReqMsgHdlr.scala
@@ -4,7 +4,6 @@ import org.bigbluebutton.common2.msgs.UserJoinMeetingAfterReconnectReqMsg
 import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers
 import org.bigbluebutton.core.apps.voice.UserJoinedVoiceConfEvtMsgHdlr
 import org.bigbluebutton.core.domain.MeetingState2x
-import org.bigbluebutton.core.models.VoiceUsers
 import org.bigbluebutton.core.running.{ BaseMeetingActor, HandlerHelpers, LiveMeeting, OutMsgRouter }
 
 trait UserJoinMeetingAfterReconnectReqMsgHdlr extends HandlerHelpers with BreakoutHdlrHelpers with UserJoinedVoiceConfEvtMsgHdlr {
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserLeaveReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserLeaveReqMsgHdlr.scala
index cfe141e6506e18c3b6b86861d062116bc15fc0af..0956c1eca41d49baa0d4f95e7b784fa6ba652e9e 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserLeaveReqMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserLeaveReqMsgHdlr.scala
@@ -2,7 +2,7 @@ package org.bigbluebutton.core.apps.users
 
 import org.bigbluebutton.common2.msgs._
 import org.bigbluebutton.core.domain.{ MeetingExpiryTracker, MeetingState2x }
-import org.bigbluebutton.core.models.{ Users2x, VoiceUserState, VoiceUsers }
+import org.bigbluebutton.core.models.Users2x
 import org.bigbluebutton.core.running.{ LiveMeeting, MeetingActor, OutMsgRouter }
 import org.bigbluebutton.core.util.TimeUtil
 import org.bigbluebutton.core2.MeetingStatus2x
@@ -38,26 +38,6 @@ trait UserLeaveReqMsgHdlr {
         // request ongoing poll to end
         handleStopPollReqMsg(u.intId)
       }
-
-      def broadcastEvent(vu: VoiceUserState): Unit = {
-        val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId,
-          vu.intId)
-        val envelope = BbbCoreEnvelope(UserLeftVoiceConfToClientEvtMsg.NAME, routing)
-        val header = BbbClientMsgHeader(UserLeftVoiceConfToClientEvtMsg.NAME, liveMeeting.props.meetingProp.intId, vu.intId)
-
-        val body = UserLeftVoiceConfToClientEvtMsgBody(voiceConf = liveMeeting.props.voiceProp.voiceConf, intId = vu.intId, voiceUserId = vu.voiceUserId)
-
-        val event = UserLeftVoiceConfToClientEvtMsg(header, body)
-        val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
-        outGW.send(msgEvent)
-      }
-
-      for {
-        user <- VoiceUsers.findWithIntId(liveMeeting.voiceUsers, msg.body.userId)
-      } yield {
-        VoiceUsers.removeWithIntId(liveMeeting.voiceUsers, user.intId)
-        broadcastEvent(user)
-      }
     }
 
     if (liveMeeting.props.meetingProp.isBreakout) {
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/UserJoinedVoiceConfEvtMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/UserJoinedVoiceConfEvtMsgHdlr.scala
index 15e35a59bf57d996fe77dee9856fa304bccf7379..33b0d4cbc528f179aebe58fe0a8bb1b7f66005ad 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/UserJoinedVoiceConfEvtMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/UserJoinedVoiceConfEvtMsgHdlr.scala
@@ -5,6 +5,7 @@ import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers
 import org.bigbluebutton.core.models.{ VoiceUserState, VoiceUsers }
 import org.bigbluebutton.core.running.{ BaseMeetingActor, LiveMeeting, OutMsgRouter }
 import org.bigbluebutton.core2.MeetingStatus2x
+import org.bigbluebutton.core2.message.senders.MsgBuilder
 
 trait UserJoinedVoiceConfEvtMsgHdlr extends BreakoutHdlrHelpers {
   this: BaseMeetingActor =>
@@ -46,6 +47,12 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends BreakoutHdlrHelpers {
     val voiceUserState = VoiceUserState(intId, voiceUserId, callingWith, callerIdName, callerIdNum, muted, talking, listenOnly = isListenOnly)
     VoiceUsers.add(liveMeeting.voiceUsers, voiceUserState)
 
+    if (MeetingStatus2x.isMeetingMuted(liveMeeting.status)) {
+      val event = MsgBuilder.buildMuteUserInVoiceConfSysMsg(liveMeeting.props.meetingProp.intId, liveMeeting.props.voiceProp.voiceConf,
+        voiceUserId, true)
+      outGW.send(event)
+    }
+
     broadcastEvent(voiceUserState)
 
     if (liveMeeting.props.meetingProp.isBreakout) {
@@ -80,4 +87,5 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends BreakoutHdlrHelpers {
     val event = StartRecordingVoiceConfSysMsg(header, body)
     BbbCommonEnvCoreMsg(envelope, event)
   }
+
 }
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 70da11e42425bb2093113b10f56de46ee52799f8..a5fb003b5736c09ac6d4f70c85706417d466dd16 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
@@ -71,15 +71,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(requesterId: String, lm: LiveMeeting): Option[PollVO] = {
     val poll = for {
       page <- lm.presModel.getCurrentPage()
@@ -101,14 +92,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)
     }
 
   }
@@ -285,14 +275,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 =>
@@ -449,7 +431,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 90c2bb537faf8ddb834d1c5941bb4e6f17ee02e3..bf642b1eda1b53ce0ae6527eb592dc0791d8501d 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
@@ -91,8 +91,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 a444195df9a790c4ee993cff68a1fbedc030f3f8..6bd0ddf833e731e48b414ca2e8ef34b66f28e206 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
@@ -218,7 +218,6 @@ class MeetingActor(
       case m: StartCustomPollReqMsg          => handleStartCustomPollReqMsg(m)
       case m: StopPollReqMsg                 => handleStopPollReqMsg(m)
       case m: ShowPollResultReqMsg           => handleShowPollResultReqMsg(m)
-      case m: HidePollResultReqMsg           => handleHidePollResultReqMsg(m)
       case m: GetCurrentPollReqMsg           => handleGetCurrentPollReqMsg(m)
       case m: RespondToPollReqMsg            => handleRespondToPollReqMsg(m)
 
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/core2/message/handlers/MuteAllExceptPresentersCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/MuteAllExceptPresentersCmdMsgHdlr.scala
index ac5259e45c2a86627941de52cb0524a0f4f4f0fe..d4da845f8730776208cffdefa637d5e8b1e0de07 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/MuteAllExceptPresentersCmdMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/MuteAllExceptPresentersCmdMsgHdlr.scala
@@ -11,28 +11,29 @@ trait MuteAllExceptPresentersCmdMsgHdlr {
   val outGW: OutMsgRouter
 
   def handleMuteAllExceptPresentersCmdMsg(msg: MuteAllExceptPresentersCmdMsg) {
-    if (MeetingStatus2x.isMeetingMuted(liveMeeting.status)) {
-      MeetingStatus2x.unmuteMeeting(liveMeeting.status)
-    } else {
-      MeetingStatus2x.muteMeeting(liveMeeting.status)
-    }
+    if (msg.body.mute != MeetingStatus2x.isMeetingMuted(liveMeeting.status)) {
+      if (msg.body.mute) {
+        MeetingStatus2x.muteMeeting(liveMeeting.status)
+      } else {
+        MeetingStatus2x.unmuteMeeting(liveMeeting.status)
+      }
 
-    val muted = MeetingStatus2x.isMeetingMuted(liveMeeting.status)
-    val event = build(props.meetingProp.intId, msg.body.mutedBy, muted, msg.body.mutedBy)
+      val muted = MeetingStatus2x.isMeetingMuted(liveMeeting.status)
+      val event = build(props.meetingProp.intId, msg.body.mutedBy, muted, msg.body.mutedBy)
 
-    outGW.send(event)
+      outGW.send(event)
 
-    // I think the correct flow would be to find those who are presenters and exclude them
-    // from the list of voice users. The remaining, mute.
-    VoiceUsers.findAll(liveMeeting.voiceUsers) foreach { vu =>
-      if (!vu.listenOnly) {
-        Users2x.findWithIntId(liveMeeting.users2x, vu.intId) match {
-          case Some(u) => if (!u.presenter) muteUserInVoiceConf(vu, muted)
-          case None    => muteUserInVoiceConf(vu, muted)
+      // I think the correct flow would be to find those who are presenters and exclude them
+      // from the list of voice users. The remaining, mute.
+      VoiceUsers.findAll(liveMeeting.voiceUsers) foreach { vu =>
+        if (!vu.listenOnly) {
+          Users2x.findWithIntId(liveMeeting.users2x, vu.intId) match {
+            case Some(u) => if (!u.presenter) muteUserInVoiceConf(vu, muted)
+            case None    => muteUserInVoiceConf(vu, muted)
+          }
         }
       }
     }
-
   }
 
   def usersWhoAreNotPresenter(): Vector[UserState] = {
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/MuteMeetingCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/MuteMeetingCmdMsgHdlr.scala
index 2329525967d7a86e0e876f50f371b5db6d96ce7b..1390ed84db7af006ed95d88e4ea25aaf5ed46566 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/MuteMeetingCmdMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/MuteMeetingCmdMsgHdlr.scala
@@ -37,20 +37,22 @@ trait MuteMeetingCmdMsgHdlr {
 
     }
 
-    if (MeetingStatus2x.isMeetingMuted(liveMeeting.status)) {
-      MeetingStatus2x.unmuteMeeting(liveMeeting.status)
-    } else {
-      MeetingStatus2x.muteMeeting(liveMeeting.status)
-    }
+    if (msg.body.mute != MeetingStatus2x.isMeetingMuted(liveMeeting.status)) {
+      if (msg.body.mute) {
+        MeetingStatus2x.muteMeeting(liveMeeting.status)
+      } else {
+        MeetingStatus2x.unmuteMeeting(liveMeeting.status)
+      }
 
-    val muted = MeetingStatus2x.isMeetingMuted(liveMeeting.status)
-    val meetingMutedEvent = build(props.meetingProp.intId, msg.body.mutedBy, muted, msg.body.mutedBy)
+      val muted = MeetingStatus2x.isMeetingMuted(liveMeeting.status)
+      val meetingMutedEvent = build(props.meetingProp.intId, msg.body.mutedBy, muted, msg.body.mutedBy)
 
-    outGW.send(meetingMutedEvent)
+      outGW.send(meetingMutedEvent)
 
-    VoiceUsers.findAll(liveMeeting.voiceUsers) foreach { vu =>
-      if (!vu.listenOnly) {
-        muteUserInVoiceConf(vu, muted)
+      VoiceUsers.findAll(liveMeeting.voiceUsers) foreach { vu =>
+        if (!vu.listenOnly) {
+          muteUserInVoiceConf(vu, muted)
+        }
       }
     }
   }
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 12e47987c18e0e8db39c6abdc6fe078d5b730bb1..78d4a90e0910b5270ff9ddafae0e294cce4dda12 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
@@ -92,6 +92,12 @@ class RedisRecorderActor(val system: ActorSystem)
       case m: RecordingStatusChangedEvtMsg          => handleRecordingStatusChangedEvtMsg(m)
       case m: EndAndKickAllSysMsg                   => handleEndAndKickAllSysMsg(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.
     }
   }
@@ -407,4 +413,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-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala
index d2de5ec711f8800e59dcc3ecd619b1f8ac6ae2ae..cdd11f2aa0105ccaf82a25fb09a432f06c0d42ec 100755
--- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala
+++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala
@@ -112,7 +112,7 @@ case class EjectUserFromVoiceCmdMsgBody(userId: String, ejectedBy: String)
 object MuteAllExceptPresentersCmdMsg { val NAME = "MuteAllExceptPresentersCmdMsg"}
 case class MuteAllExceptPresentersCmdMsg(header: BbbClientMsgHeader,
                           body: MuteAllExceptPresentersCmdMsgBody) extends StandardMsg
-case class MuteAllExceptPresentersCmdMsgBody(mutedBy: String)
+case class MuteAllExceptPresentersCmdMsgBody(mutedBy: String, mute: Boolean)
 
 /**
   * Sent by client to mute all users except presenters in the voice conference.
@@ -133,7 +133,7 @@ case class IsMeetingMutedRespMsgBody(muted: Boolean)
 object MuteUserCmdMsg { val NAME = "MuteUserCmdMsg"}
 case class MuteUserCmdMsg(header: BbbClientMsgHeader,
                                     body: MuteUserCmdMsgBody) extends StandardMsg
-case class MuteUserCmdMsgBody(userId: String, mutedBy: String)
+case class MuteUserCmdMsgBody(userId: String, mutedBy: String, mute: Boolean)
 
 /**
   * Sent to FS to get the users in the voice conference.
@@ -165,7 +165,7 @@ case class GetUsersInVoiceConfSysMsgBody(voiceConf: String)
 object MuteMeetingCmdMsg { val NAME = "MuteMeetingCmdMsg" }
 case class MuteMeetingCmdMsg(header: BbbClientMsgHeader,
                                   body: MuteMeetingCmdMsgBody) extends StandardMsg
-case class MuteMeetingCmdMsgBody(mutedBy: String)
+case class MuteMeetingCmdMsgBody(mutedBy: String, mute: Boolean)
 
 /**
   * Send to all clients that meeting is muted.
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 7f6ae4a21f871cc37131ed53ba5f71a4fe008f33..d8d6bc963b86632c74a859d4556c11770d18e753 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
@@ -70,6 +70,9 @@ public class ParamsProcessorUtil {
     private String defaultServerUrl;
     private int defaultNumDigitsForTelVoice;
     private String defaultClientUrl;
+    private String html5ClientUrl;
+    private Boolean moderatorsJoinViaHTML5Client;
+    private Boolean attendeesJoinViaHTML5Client;
     private String defaultAvatarURL;
     private String defaultConfigURL;
     private String defaultGuestPolicy;
@@ -349,6 +352,9 @@ public class ParamsProcessorUtil {
         int meetingDuration = processMeetingDuration(params.get("duration"));
         int logoutTimer = processMeetingDuration(params.get("logoutTimer"));
 
+        // Hardcode to zero as we don't use this feature in 2.0.x (ralam dec 18, 2017)
+		logoutTimer = 0;
+
         // set is breakout room property
         boolean isBreakout = false;
         if (!StringUtils.isEmpty(params.get("isBreakout"))) {
@@ -460,7 +466,9 @@ public class ParamsProcessorUtil {
         meeting.storeConfig(true, configXML);
 
         if (!StringUtils.isEmpty(params.get("moderatorOnlyMessage"))) {
-            String moderatorOnlyMessage = params.get("moderatorOnlyMessage");
+            String moderatorOnlyMessageTemplate = params.get("moderatorOnlyMessage");
+			String moderatorOnlyMessage = substituteKeywords(moderatorOnlyMessageTemplate,
+					dialNumber, telVoice, meetingName);
             meeting.setModeratorOnlyMessage(moderatorOnlyMessage);
         }
 
@@ -475,6 +483,14 @@ public class ParamsProcessorUtil {
             meeting.setParentMeetingId(parentMeetingId);
         }
 
+		if (!StringUtils.isEmpty(params.get("logo"))) {
+			meeting.setCustomLogoURL(params.get("logo"));
+		}
+
+		if (!StringUtils.isEmpty(params.get("copyright"))) {
+			meeting.setCustomCopyright(params.get("copyright"));
+		}
+
         return meeting;
     }
 	
@@ -489,7 +505,19 @@ public class ParamsProcessorUtil {
 	public String getDefaultClientUrl() {
 		return defaultClientUrl;
 	}
-	
+
+	public String getHTML5ClientUrl() {
+		return html5ClientUrl;
+	}
+
+	public Boolean getAttendeesJoinViaHTML5Client() {
+		return attendeesJoinViaHTML5Client;
+	}
+
+	public Boolean getModeratorsJoinViaHTML5Client() {
+		return moderatorsJoinViaHTML5Client;
+	}
+
 	public String getDefaultConfigXML() {
 		defaultConfigXML = getConfig(defaultConfigURL);
 		
@@ -808,6 +836,18 @@ public class ParamsProcessorUtil {
 		this.defaultClientUrl = defaultClientUrl;
 	}
 
+	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) {
 		this.defaultMeetingDuration = defaultMeetingDuration;
 	}
diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java
index cd0fd6c9394bb8b4c33e28c622c343529637dd12..39c5ab8dfb299bf3fdc8682317c853114de56f4b 100755
--- a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java
+++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java
@@ -63,6 +63,8 @@ public class Meeting {
 	private final ConcurrentMap<String, Config> configs;
 	private final Boolean isBreakout;
 	private final List<String> breakoutRooms = new ArrayList<String>();
+	private String customLogoURL = "";
+	private String customCopyright = "";
 
 	private Integer maxInactivityTimeoutMinutes = 120;
 	private Integer warnMinutesBeforeMax = 5;
@@ -292,7 +294,23 @@ public class Meeting {
 	public boolean hasUserJoined() {
 		return userHasJoined;
 	}
-	
+
+	public String getCustomLogoURL() {
+		return customLogoURL;
+	}
+
+	public void setCustomLogoURL(String url) {
+		customLogoURL = url;
+	}
+
+	public void setCustomCopyright(String copyright) {
+    	customCopyright = copyright;
+	}
+
+	public String getCustomCopyright() {
+    	return customCopyright;
+	}
+
 	public void userJoined(User user) {
 	    userHasJoined = true;
 	    this.users.put(user.getInternalUserId(), user);
diff --git a/bbb-lti/.asscache b/bbb-lti/.asscache
deleted file mode 100644
index be6deceb5e9795140859e536f78de8fe794cf1ef..0000000000000000000000000000000000000000
Binary files a/bbb-lti/.asscache and /dev/null differ
diff --git a/bbb-lti/.gitignore b/bbb-lti/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..b1bf7c47d2acad6cf5cb5bab1e95b7914a7e881f
--- /dev/null
+++ b/bbb-lti/.gitignore
@@ -0,0 +1 @@
+.asscache
\ No newline at end of file
diff --git a/bbb-lti/application.properties b/bbb-lti/application.properties
index 68fe1dc6a3086a523d0d680d0a2aa94498698311..a00806dd9e3917149f9cd48666f6b864737f9235 100644
--- a/bbb-lti/application.properties
+++ b/bbb-lti/application.properties
@@ -2,4 +2,5 @@
 #Fri Aug 19 19:12:11 UTC 2016
 app.grails.version=2.5.2
 app.name=lti
-app.version=0.3
+app.servlet.version=3.0
+app.version=0.4
diff --git a/bbb-lti/grails-app/assets/javascripts/tool.js b/bbb-lti/grails-app/assets/javascripts/tool.js
index 4f9da332264fd75ad2c5147bf6a547088e1af0b8..b5ad9cc65819349d8d1073531ca109578234d2b5 100644
--- a/bbb-lti/grails-app/assets/javascripts/tool.js
+++ b/bbb-lti/grails-app/assets/javascripts/tool.js
@@ -15,26 +15,33 @@
     You should have received a copy of the GNU Lesser General Public License along
     with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 */
-$(document).ready( function () {
+$(document).ready(function() {
 	if (typeof jQuery !== 'undefined') {
 		(function($) {
 			$('[data-toggle="confirmation"]').confirmation({popout: true});
 			$('#recordings').dataTable({
 				columnDefs: [ {
-					targets: 3,
-					render: $.fn.dataTable.render.moment('X', 'LLL', locale)
+					targets: 4,
+					render: $.fn.dataTable.render.moment('X', 'lll', locale)
 				} ],
 				sPaginationType : "full_numbers",
 				"columns": [
 					null,
 					null,
 					null,
+					{ "width": "90px" },
 					null,
-					null,
-					{ "width": "160px" }
+					{ "width": "40px" },
+					{ "width": "120px" }
 				],
-				"order": [[ 3, "desc" ]]
+				"order": [[ 4, "desc" ]]
 			});
+			$(".glyphicon-eye-open").hover(function() {
+        $(this).toggleClass('glyphicon-eye-open glyphicon-eye-close');
+    	});
+			$(".glyphicon-eye-close").hover(function() {
+        $(this).toggleClass('glyphicon-eye-close glyphicon-eye-open');
+    	});
 		})(jQuery);
-	}
+	};
 });
diff --git a/bbb-lti/grails-app/assets/stylesheets/tool.css b/bbb-lti/grails-app/assets/stylesheets/tool.css
new file mode 100644
index 0000000000000000000000000000000000000000..a0e939bb962772676662774d3bedd24b67ffabe4
--- /dev/null
+++ b/bbb-lti/grails-app/assets/stylesheets/tool.css
@@ -0,0 +1,14 @@
+.thumbnail {
+    height: 51px;
+    padding: 4px;
+    width: 66px;
+    float: left;
+}
+
+.thumbnail:hover {
+    display: inline-block;
+    height: auto;
+    position: absolute;
+    width: auto;
+    z-index: 99999;
+}
diff --git a/bbb-lti/grails-app/conf/BuildConfig.groovy b/bbb-lti/grails-app/conf/BuildConfig.groovy
index 0bf647fec3f6cb2567a104182888511ffe2e6c43..4a84c2785ab79613b455443f1f6ee09cdabe83cd 100644
--- a/bbb-lti/grails-app/conf/BuildConfig.groovy
+++ b/bbb-lti/grails-app/conf/BuildConfig.groovy
@@ -1,4 +1,4 @@
-/* 
+/*
     BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
 
     Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
@@ -14,7 +14,7 @@
 
     You should have received a copy of the GNU Lesser General Public License along
     with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
-*/    
+*/
 
 grails.servlet.version = "3.0" // Change depending on target container compliance (2.5 or 3.0)
 grails.project.class.dir = "target/classes"
@@ -65,6 +65,7 @@ grails.project.dependency.resolution = {
     }
 
     dependencies {
+        compile 'org.json:json:20171018'
         // specify dependencies here under either 'build', 'compile', 'runtime', 'test' or 'provided' scopes e.g.
         // runtime 'mysql:mysql-connector-java:5.1.29'
         // runtime 'org.postgresql:postgresql:9.3-1101-jdbc41'
diff --git a/bbb-lti/grails-app/conf/Config.groovy b/bbb-lti/grails-app/conf/Config.groovy
index 40ca78588a826c528ffc3ddcb9782db2082841fb..966ea2329ee7c256de724ec2e5bb1e5d0a0aba8f 100644
--- a/bbb-lti/grails-app/conf/Config.groovy
+++ b/bbb-lti/grails-app/conf/Config.groovy
@@ -1,4 +1,4 @@
-/* 
+/*
     BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
 
     Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
@@ -14,7 +14,7 @@
 
     You should have received a copy of the GNU Lesser General Public License along
     with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
-*/    
+*/
 
 // locations to search for config files that get merged into the main config;
 // config files can be ConfigSlurper scripts, Java properties files, or classes
@@ -103,6 +103,9 @@ grails.hibernate.pass.readonly = false
 // configure passing read-only to OSIV session by default, requires "singleSession = false" OSIV mode
 grails.hibernate.osiv.readonly = false
 
+// Enable hot reloading for production environments
+grails.gsp.enable.reload=true
+
 environments {
     development {
         grails.logging.jul.usebridge = true
diff --git a/bbb-lti/grails-app/controllers/org/bigbluebutton/ToolController.groovy b/bbb-lti/grails-app/controllers/org/bigbluebutton/ToolController.groovy
index 600218fedfb3fb3544939bf3798492c7fb3d19d5..87a85f90519459ce007461fb734e0325f99b19fb 100644
--- a/bbb-lti/grails-app/controllers/org/bigbluebutton/ToolController.groovy
+++ b/bbb-lti/grails-app/controllers/org/bigbluebutton/ToolController.groovy
@@ -48,125 +48,103 @@ class ToolController {
 
     def index = {
         log.debug CONTROLLER_NAME + "#index"
-        if( ltiService.consumerMap == null) ltiService.initConsumerMap()
-
+        if (ltiService.consumerMap == null) {
+            ltiService.initConsumerMap()
+        }
         setLocalization(params)
-
         params.put(REQUEST_METHOD, request.getMethod().toUpperCase())
         ltiService.logParameters(params)
-
-        if( request.post ){
-            def scheme = request.isSecure()? "https": "http"
-            def endPoint = scheme + "://" + ltiService.endPoint + "/" + grailsApplication.metadata['app.name'] + "/" + params.get("controller") + (params.get("format") != null? "." + params.get("format"): "")
-            log.info "endPoint: " + endPoint
-            Map<String, String> result = new HashMap<String, String>()
-            ArrayList<String> missingParams = new ArrayList<String>()
-
-            if (hasAllRequiredParams(params, missingParams)) {
-                def sanitizedParams = sanitizePrametersForBaseString(params)
-                def consumer = ltiService.getConsumer(params.get(Parameter.CONSUMER_ID))
-                if ( !ltiService.hasRestrictedAccess() || consumer != null) {
-                    if (ltiService.hasRestrictedAccess() ) {
-                        log.debug "Found consumer with key " + consumer.get("key") //+ " and sharedSecret " + consumer.get("secret")
-                    }
-
-                    if (!ltiService.hasRestrictedAccess() || checkValidSignature(params.get(REQUEST_METHOD), endPoint, consumer.get("secret"), sanitizedParams, params.get(Parameter.OAUTH_SIGNATURE))) {
-                        if (!ltiService.hasRestrictedAccess() ) {
-                            log.debug  "Access not restricted, valid signature is not required."
-                        } else {
-                            log.debug  "The message has a valid signature."
-                        }
-
-                        def mode = params.containsKey(Parameter.CUSTOM_MODE)? params.get(Parameter.CUSTOM_MODE): ltiService.mode
-                        if( !"extended".equals(mode) ) {
-                            log.debug  "LTI service running in simple mode."
-                            result = doJoinMeeting(params)
-                        } else {
-                            log.debug  "LTI service running in extended mode."
-                            if ( !Boolean.parseBoolean(params.get(Parameter.CUSTOM_RECORD)) && !ltiService.allRecordedByDefault() ) {
-                                log.debug  "Parameter custom_record was not sent; immediately redirecting to BBB session!"
-                                result = doJoinMeeting(params)
-                            }
-                        }
-
-                    } else {
-                        log.debug  "The message has NOT a valid signature."
-                        result.put("resultMessageKey", "InvalidSignature")
-                        result.put("resultMessage", "Invalid signature (" + params.get(Parameter.OAUTH_SIGNATURE) + ").")
-                    }
-
-                } else {
-                    result.put("resultMessageKey", "ConsumerNotFound")
-                    result.put("resultMessage", "Consumer with id = " + params.get(Parameter.CONSUMER_ID) + " was not found.")
-                }
-
-            } else {
-                String missingStr = ""
-                for(String str:missingParams) {
-                    missingStr += str + ", ";
-                }
-                result.put("resultMessageKey", "MissingRequiredParameter")
-                result.put("resultMessage", "Missing parameters [$missingStr]")
+        // On get requests render the common cartridge.
+        if (request.get) {
+            render(text: getCartridgeXML(), contentType: "text/xml", encoding: "UTF-8")
+            return
+        }
+        // On post request proceed with the launch.
+        def endPoint = ltiService.getScheme(request) + "://" + ltiService.endPoint + "/" + grailsApplication.metadata['app.name'] + "/" + params.get("controller") + (params.get("format") != null ? "." + params.get("format") : "")
+        log.info "endPoint: " + endPoint
+        ArrayList<String> missingParams = new ArrayList<String>()
+
+        if (!hasAllRequiredParams(params, missingParams)) {
+            String missingStr = ""
+            for (String str:missingParams) {
+                missingStr += str + ", ";
             }
+            return renderError("MissingRequiredParameter", "Missing parameters [$missingStr]")
+        }
 
-            if( result.containsKey("resultMessageKey") ) {
-                log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
-                render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
-
-            } else {
-                session["params"] = params
-                render(view: "index", model: ['params': params, 'recordingList': getSanitizedRecordings(params), 'ismoderator': bigbluebuttonService.isModerator(params)])
+        def sanitizedParams = sanitizePrametersForBaseString(params)
+        def consumer = ltiService.getConsumer(params.get(Parameter.CONSUMER_ID))
+        if (ltiService.hasRestrictedAccess()) {
+            if (consumer == null) {
+                return renderError("ConsumerNotFound", "Consumer with id = " + params.get(Parameter.CONSUMER_ID) + " was not found.")
+            }
+            log.debug "Found consumer with key " + consumer.get("key") //+ " and sharedSecret " + consumer.get("secret")
+        }
+        def validSignature = checkValidSignature(params.get(REQUEST_METHOD), endPoint, consumer.get("secret"), sanitizedParams, params.get(Parameter.OAUTH_SIGNATURE))
+        if (ltiService.hasRestrictedAccess()) {
+            if (!validSignature) {
+                log.debug  "The message has NOT a valid signature."
+                return renderError("InvalidSignature", "Invalid signature (" + params.get(Parameter.OAUTH_SIGNATURE) + ").")
             }
+            log.debug  "The message has a valid signature."
         } else {
-            render(text: getCartridgeXML(), contentType: "text/xml", encoding: "UTF-8")
+            log.debug  "Access not restricted, valid signature is not required."
+        }
+        def mode = params.containsKey(Parameter.CUSTOM_MODE)? params.get(Parameter.CUSTOM_MODE): ltiService.mode
+        if (!"extended".equals(mode)) {
+            log.debug  "LTI service running in simple mode."
+            def result = doJoinMeeting(params)
+            return
         }
+        log.debug  "LTI service running in extended mode."
+        if (!Boolean.parseBoolean(params.get(Parameter.CUSTOM_RECORD)) && !ltiService.allRecordedByDefault()) {
+            log.debug  "Parameter custom_record was not sent; immediately redirecting to BBB session!"
+            def result = doJoinMeeting(params)
+            return
+        }
+        session["params"] = params
+        render(view: "index", model: ['params': params, 'recordingList': getSanitizedRecordings(params), 'ismoderator': bigbluebuttonService.isModerator(params)])
     }
 
     def join = {
         if( ltiService.consumerMap == null) ltiService.initConsumerMap()
         log.debug CONTROLLER_NAME + "#join"
-        Map<String, String> result
-
+        def result
         def sessionParams = session["params"]
-
         if( sessionParams != null ) {
             log.debug "params: " + params
             log.debug "sessionParams: " + sessionParams
             result = doJoinMeeting(sessionParams)
         } else {
             result = new HashMap<String, String>()
-            result.put("resultMessageKey", "InvalidSession")
-            result.put("resultMessage", "Invalid session. User can not execute this action.")
+            result.put("messageKey", "InvalidSession")
+            result.put("message", "Invalid session. User can not execute this action.")
         }
-
-        if( result.containsKey("resultMessageKey")) {
-            log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
-            render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
+        if (result != null && result.containsKey("messageKey")) {
+            log.debug "Error [messageKey:'" + result.get("messageKey") + "', message:'" + result.get("message") + "']"
+            render(view: "error", model: ['messageKey': result.get("messageKey"), 'message': result.get("message")])
         }
     }
 
     def publish = {
         log.debug CONTROLLER_NAME + "#publish"
         Map<String, String> result
-
         def sessionParams = session["params"]
-
         if( sessionParams == null ) {
             result = new HashMap<String, String>()
-            result.put("resultMessageKey", "InvalidSession")
-            result.put("resultMessage", "Invalid session. User can not execute this action.")
+            result.put("messageKey", "InvalidSession")
+            result.put("message", "Invalid session. User can not execute this action.")
         } else if ( !bigbluebuttonService.isModerator(sessionParams) ) {
             result = new HashMap<String, String>()
-            result.put("resultMessageKey", "NotAllowed")
-            result.put("resultMessage", "User not allowed to execute this action.")
+            result.put("messageKey", "NotAllowed")
+            result.put("message", "User not allowed to execute this action.")
         } else {
-            //Execute the publish command
+            // Execute the publish command
             result = bigbluebuttonService.doPublishRecordings(params)
         }
-
-        if( result.containsKey("resultMessageKey")) {
-            log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
-            render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
+        if( result.containsKey("messageKey")) {
+            log.debug "Error [messageKey:'" + result.get("messageKey") + "', message:'" + result.get("message") + "']"
+            render(view: "error", model: ['messageKey': result.get("messageKey"), 'message': result.get("message")])
         } else {
             render(view: "index", model: ['params': sessionParams, 'recordingList': getSanitizedRecordings(sessionParams), 'ismoderator': bigbluebuttonService.isModerator(sessionParams)])
         }
@@ -175,25 +153,22 @@ class ToolController {
     def delete = {
         log.debug CONTROLLER_NAME + "#delete"
         Map<String, String> result
-
         def sessionParams = session["params"]
-
         if( sessionParams == null ) {
             result = new HashMap<String, String>()
-            result.put("resultMessageKey", "InvalidSession")
-            result.put("resultMessage", "Invalid session. User can not execute this action.")
+            result.put("messageKey", "InvalidSession")
+            result.put("message", "Invalid session. User can not execute this action.")
         } else if ( !bigbluebuttonService.isModerator(sessionParams) ) {
             result = new HashMap<String, String>()
-            result.put("resultMessageKey", "NotAllowed")
-            result.put("resultMessage", "User not allowed to execute this action.")
+            result.put("messageKey", "NotAllowed")
+            result.put("message", "User not allowed to execute this action.")
         } else {
-            //Execute the delete command
+            // Execute the delete command.
             result = bigbluebuttonService.doDeleteRecordings(params)
         }
-
-        if( result.containsKey("resultMessageKey")) {
-            log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
-            render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
+        if( result.containsKey("messageKey")) {
+            log.debug "Error [messageKey:'" + result.get("messageKey") + "', message:'" + result.get("message") + "']"
+            render(view: "error", model: ['messageKey': result.get("messageKey"), 'message': result.get("message")])
         } else {
             render(view: "index", model: ['params': sessionParams, 'recordingList': getSanitizedRecordings(sessionParams), 'ismoderator': bigbluebuttonService.isModerator(sessionParams)])
         }
@@ -203,48 +178,39 @@ class ToolController {
         String locale = params.get(Parameter.LAUNCH_LOCALE)
         locale = (locale == null || locale.equals("")?"en":locale)
         String[] localeCodes = locale.split("_")
-        //Localize the default welcome message
-        if( localeCodes.length > 1 )
+        // Localize the default welcome message
+        session['org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE'] = new Locale(localeCodes[0])
+        if (localeCodes.length > 1) {
             session['org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE'] = new Locale(localeCodes[0], localeCodes[1])
-        else
-            session['org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE'] = new Locale(localeCodes[0])
+        }
     }
 
     private Object doJoinMeeting(Map<String, String> params) {
-        Map<String, String> result = new HashMap<String, String>()
-
         setLocalization(params)
         String welcome = message(code: "bigbluebutton.welcome.header", args: ["\"{0}\"", "\"{1}\""]) + "<br>"
-
         // Check for [custom_]welcome parameter being passed from the LTI
-        if ( params.containsKey(Parameter.CUSTOM_WELCOME) && params.get(Parameter.CUSTOM_WELCOME) != null ) {
+        if (params.containsKey(Parameter.CUSTOM_WELCOME) && params.get(Parameter.CUSTOM_WELCOME) != null) {
             welcome = params.get(Parameter.CUSTOM_WELCOME) + "<br>"
             log.debug "Overriding default welcome message with: [" + welcome + "]"
         }
-
-        if ( params.containsKey(Parameter.CUSTOM_RECORD) && Boolean.parseBoolean(params.get(Parameter.CUSTOM_RECORD)) || ltiService.allRecordedByDefault() ) {
+        if (params.containsKey(Parameter.CUSTOM_RECORD) && Boolean.parseBoolean(params.get(Parameter.CUSTOM_RECORD)) || ltiService.allRecordedByDefault()) {
             welcome += "<br><b>" + message(code: "bigbluebutton.welcome.record") + "</b><br>"
             log.debug "Adding record warning to welcome message, welcome is now: [" + welcome + "]"
         }
-
-        if ( params.containsKey(Parameter.CUSTOM_DURATION) && Integer.parseInt(params.get(Parameter.CUSTOM_DURATION)) > 0 ) {
+        if (params.containsKey(Parameter.CUSTOM_DURATION) && Integer.parseInt(params.get(Parameter.CUSTOM_DURATION)) > 0) {
             welcome += "<br><b>" + message(code: "bigbluebutton.welcome.duration", args: [params.get(Parameter.CUSTOM_DURATION)]) + "</b><br>"
             log.debug "Adding duration warning to welcome message, welcome is now: [" + welcome + "]"
         }
-
         welcome += "<br>" + message(code: "bigbluebutton.welcome.footer") + "<br>"
-
         String destinationURL = bigbluebuttonService.getJoinURL(params, welcome, ltiService.mode)
-        log.debug "redirecting to " + destinationURL
-
-        if( destinationURL != null ) {
-            redirect(url:destinationURL)
-        } else {
-            result.put("resultMessageKey", "BigBlueButtonServerError")
-            result.put("resultMessage", "The join could not be completed")
+        if (destinationURL == null) {
+            Map<String, String> result = new HashMap<String, String>()
+            result.put("messageKey", "BigBlueButtonServerError")
+            result.put("message", "The join could not be completed")
+            return result
         }
-
-        return result
+        log.debug "It is redirecting to " + destinationURL
+        redirect(url:destinationURL)
     }
 
     /**
@@ -258,14 +224,15 @@ class ToolController {
             if (key == "action" || key == "controller" || key == "format") {
                 // Ignore as these are the grails controller and action tied to this request.
                 continue
-            } else if (key == "oauth_signature") {
-                // We don't need this as part of the base string
+            }
+            if (key == "oauth_signature") {
+                // We don't need this as part of the base string.
                 continue
-            } else if (key == "request_method") {
-                // As this is was added by the controller, we don't want it as part of the base string
+            }
+            if (key == "request_method") {
+                // As this is was added by the controller, we don't want it as part of the base string.
                 continue
             }
-
             reqProp.setProperty(key, params.get(key));
         }
         return reqProp
@@ -279,24 +246,19 @@ class ToolController {
      */
     private boolean hasAllRequiredParams(Map<String, String> params, ArrayList<String> missingParams) {
         log.debug "Checking for required parameters"
-
-        boolean hasAllParams = true
-        if ( ltiService.hasRestrictedAccess() && !params.containsKey(Parameter.CONSUMER_ID) ) {
+        if (ltiService.hasRestrictedAccess() && !params.containsKey(Parameter.CONSUMER_ID)) {
             missingParams.add(Parameter.CONSUMER_ID);
-            hasAllParams = false;
+            return false
         }
-
-        if ( ltiService.hasRestrictedAccess() && !params.containsKey(Parameter.OAUTH_SIGNATURE)) {
+        if (ltiService.hasRestrictedAccess() && !params.containsKey(Parameter.OAUTH_SIGNATURE)) {
             missingParams.add(Parameter.OAUTH_SIGNATURE);
-            hasAllParams = false;
+            return false
         }
-
-        if ( !params.containsKey(Parameter.RESOURCE_LINK_ID) ) {
+        if (!params.containsKey(Parameter.RESOURCE_LINK_ID)) {
             missingParams.add(Parameter.RESOURCE_LINK_ID);
-            hasAllParams = false;
+            return false
         }
-
-        return hasAllParams
+        return true
     }
 
     /**
@@ -309,32 +271,23 @@ class ToolController {
      * @return - TRUE if the signatures matches the calculated signature
      */
     private boolean checkValidSignature(String method, String url, String conSecret, Properties postProp, String signature) {
-        def validSignature = false
-
-        if ( ltiService.hasRestrictedAccess() ) {
-            try {
-                OAuthMessage oam = new OAuthMessage(method, url, postProp.entrySet())
-                //log.debug "OAuthMessage oam = " + oam.toString()
-
-                HMAC_SHA1 hmac = new HMAC_SHA1()
-                //log.debug "HMAC_SHA1 hmac = " + hmac.toString()
-
-                hmac.setConsumerSecret(conSecret)
-
-                log.debug "Base Message String = [ " + hmac.getBaseString(oam) + " ]\n"
-                String calculatedSignature = hmac.getSignature(hmac.getBaseString(oam))
-                log.debug "Calculated: " + calculatedSignature + " Received: " + signature
-
-                validSignature = calculatedSignature.equals(signature)
-            } catch( Exception e ) {
-                log.debug "Exception error: " + e.message
-            }
-
-        } else {
-            validSignature = true
+        if (!ltiService.hasRestrictedAccess()) {
+            return true;
+        }
+        try {
+            OAuthMessage oam = new OAuthMessage(method, url, postProp.entrySet())
+            //log.debug "OAuthMessage oam = " + oam.toString()
+            HMAC_SHA1 hmac = new HMAC_SHA1()
+            //log.debug "HMAC_SHA1 hmac = " + hmac.toString()
+            hmac.setConsumerSecret(conSecret)
+            log.debug "Base Message String = [ " + hmac.getBaseString(oam) + " ]\n"
+            String calculatedSignature = hmac.getSignature(hmac.getBaseString(oam))
+            log.debug "Calculated: " + calculatedSignature + " Received: " + signature
+            return calculatedSignature.equals(signature)
+        } catch( Exception e ) {
+            log.debug "Exception error: " + e.message
+            return false
         }
-
-        return validSignature
     }
 
     /**
@@ -343,26 +296,50 @@ class ToolController {
      * @return the key:val pairs needed for Basic LTI
      */
     private List<Object> getSanitizedRecordings(Map<String, String> params) {
-        List<Object> recordings = bigbluebuttonService.getRecordings(params)
-        for(Map<String, Object> recording: recordings){
-            /// Calculate duration
+        def recordings = new ArrayList<Object>()
+        def getRecordingsResponse = bigbluebuttonService.getRecordings(params)
+        if (getRecordingsResponse == null) {
+            return recordings
+        }
+        Object response = (Object)getRecordingsResponse.get("recording")
+        if (response instanceof Map<?,?>) {
+            recordings.add(response)
+        }
+        if (response instanceof Collection<?>) {
+            recordings = response
+        }
+        // Sanitize recordings
+        for (recording in recordings) {
+            // Calculate duration.
             long endTime = Long.parseLong((String)recording.get("endTime"))
             endTime -= (endTime % 1000)
             long startTime = Long.parseLong((String)recording.get("startTime"))
             startTime -= (startTime % 1000)
             int duration = (endTime - startTime) / 60000
-            /// Add duration
+            // Add duration.
             recording.put("duration", duration )
-            /// Calculate reportDate
+            // Calculate reportDate.
             DateFormat df = new SimpleDateFormat(message(code: "tool.view.dateFormat"))
             String reportDate = df.format(new Date(startTime))
-            /// Add reportDate
+            // Add reportDate.
             recording.put("reportDate", reportDate)
             recording.put("unixDate", startTime / 1000)
+            // Add sanitized thumbnails
+            recording.put("thumbnails", sanitizeThumbnails(recording.playback.format))
         }
         return recordings
     }
 
+    private List<Object> sanitizeThumbnails(Object format) {
+        if (format.preview == null || format.preview.images == null || format.preview.images.image == null) {
+            return new ArrayList()
+        }
+        if (format.preview.images.image instanceof Map<?,?>) {
+            return new ArrayList(format.preview.images.image)
+        }
+        return format.preview.images.image
+    }
+
     private String getCartridgeXML(){
         def lti_endpoint = ltiService.retrieveBasicLtiEndpoint() + '/' + grailsApplication.metadata['app.name']
         def launch_url = 'http://' + lti_endpoint + '/tool'
@@ -399,4 +376,9 @@ class ToolController {
 
         return cartridge
     }
+
+    private void renderError(key, message) {
+        log.debug "Error [resultMessageKey:'" + key + "', resultMessage:'" + message + "']"
+        render(view: "error", model: ['resultMessageKey': key, 'resultMessage': message])
+    }
 }
diff --git a/bbb-lti/grails-app/i18n/messages.properties b/bbb-lti/grails-app/i18n/messages.properties
index a9a014740524315d68b6ab5d1e78b046d2f031de..4b708fbecba231e618ff510beff21645a3ccdddd 100644
--- a/bbb-lti/grails-app/i18n/messages.properties
+++ b/bbb-lti/grails-app/i18n/messages.properties
@@ -17,7 +17,7 @@
 #
 
 # The welcome.header can be static, however if you want the name of the activity (meeting) to be injected use {0} as part of the text
-# {1} can be used to inject the name of the course 
+# {1} can be used to inject the name of the course
 bigbluebutton.welcome.header=Welcome to <b>{0}</b>!
 bigbluebutton.welcome.footer=To understand how BigBlueButton works see our <a href=\"event:http://www.bigbluebutton.org/content/videos\"><u>tutorial videos</u></a>.<br><br>To join the audio bridge click the headset icon (upper-left hand corner). <b>Please use a headset to avoid causing noise for others.
 bigbluebutton.welcome.record=This meeting is being recorded
@@ -38,9 +38,10 @@ tool.view.recording.unpublish=Unpublish
 tool.view.recording.delete=Delete
 tool.view.activity=Activity
 tool.view.description=Description
+tool.view.preview=Preview
 tool.view.date=Date
 tool.view.duration=Duration
 tool.view.actions=Actions
-tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z
+tool.view.dateFormat=E, MM dd, yyyy HH:mm:ss Z
 
-tool.error.general=Connection could not be established.
\ No newline at end of file
+tool.error.general=Connection could not be established.
diff --git a/bbb-lti/grails-app/i18n/messages_es.properties b/bbb-lti/grails-app/i18n/messages_es.properties
index 7e0eedbf335ba1e69dffed503faf3e860a44f3e0..2513f516b2906e9dec726a59a4225b176dd32311 100644
--- a/bbb-lti/grails-app/i18n/messages_es.properties
+++ b/bbb-lti/grails-app/i18n/messages_es.properties
@@ -39,9 +39,10 @@ tool.view.recording.confirmation.yes=Si
 tool.view.recording.confirmation.no=No
 tool.view.activity=Actividad
 tool.view.description=Descripci&#243;n
+tool.view.preview=Vista preliminar
 tool.view.date=Fecha
 tool.view.duration=Duraci&#243;n
 tool.view.actions=Acciones
-tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z
+tool.view.dateFormat=E, MM dd, yyyy HH:mm:ss Z
 
-tool.error.general=No pudo estableserce la conexi&#243;n.
\ No newline at end of file
+tool.error.general=No pudo estableserce la conexi&#243;n.
diff --git a/bbb-lti/grails-app/i18n/messages_fr.properties b/bbb-lti/grails-app/i18n/messages_fr.properties
index 70c9fd7fbda9b803d3c1ecbda3db82b568235148..a25b69bf01283a26867cecec4706e18ef265e11a 100644
--- a/bbb-lti/grails-app/i18n/messages_fr.properties
+++ b/bbb-lti/grails-app/i18n/messages_fr.properties
@@ -1,42 +1,43 @@
-#
-# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
-#
-# Copyright (c) 2012 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/>.
-#
-
-bigbluebutton.welcome.header=<br>Bienvenue au <b>{0}</b>!<br>
-bigbluebutton.welcome.footer=<br>Pour comprendre comment fonctionne BigBlueButton, consultez les <a href=\"event:http://www.bigbluebutton.org/content/videos\"><u>didacticiels vid&#233;o</u></a>.<br><br>Pour activer l'audio cliquez sur l'ic&#244;ne du casque &#224; &#233;couteurs (coin sup&#233;rieur gauche). <b>S'il vous pla�t utiliser le casque pour &#233;viter de causer du bruit.</b>
-
-tool.view.app=BigBlueButton
-tool.view.title=LTI Interface pour BigBlueButton
-tool.view.join=Saisie de la r&#233;union
-tool.view.recording=Enregistrement
-tool.view.recording.format.presentation=presentation
-tool.view.recording.format.video=video
-tool.view.recording.delete.confirmation=Veillez &#224; supprimer d&#233;finitivement cet enregistrement?
-tool.view.recording.delete.confirmation.warning=Attention
-tool.view.recording.delete.confirmation.yes=Oui
-tool.view.recording.delete.confirmation.no=Non
-tool.view.recording.publish=Publier
-tool.view.recording.unpublish=D&#233;publier
-tool.view.recording.delete=Supprimer
-tool.view.activity=Activit&#233;
-tool.view.description=Description
-tool.view.date=Date
-tool.view.duration=Dur&#233;e
-tool.view.actions=Actions
-tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z
-
-tool.error.general=Pas possible &#233;tablir la connection.
\ No newline at end of file
+#
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
+#
+# Copyright (c) 2012 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/>.
+#
+
+bigbluebutton.welcome.header=<br>Bienvenue au <b>{0}</b>!<br>
+bigbluebutton.welcome.footer=<br>Pour comprendre comment fonctionne BigBlueButton, consultez les <a href=\"event:http://www.bigbluebutton.org/content/videos\"><u>didacticiels vid&#233;o</u></a>.<br><br>Pour activer l'audio cliquez sur l'ic&#244;ne du casque &#224; &#233;couteurs (coin sup&#233;rieur gauche). <b>S'il vous pla�t utiliser le casque pour &#233;viter de causer du bruit.</b>
+
+tool.view.app=BigBlueButton
+tool.view.title=LTI Interface pour BigBlueButton
+tool.view.join=Saisie de la r&#233;union
+tool.view.recording=Enregistrement
+tool.view.recording.format.presentation=presentation
+tool.view.recording.format.video=video
+tool.view.recording.delete.confirmation=Veillez &#224; supprimer d&#233;finitivement cet enregistrement?
+tool.view.recording.delete.confirmation.warning=Attention
+tool.view.recording.delete.confirmation.yes=Oui
+tool.view.recording.delete.confirmation.no=Non
+tool.view.recording.publish=Publier
+tool.view.recording.unpublish=D&#233;publier
+tool.view.recording.delete=Supprimer
+tool.view.activity=Activit&#233;
+tool.view.description=Description
+tool.view.preview=Apre&#231;u
+tool.view.date=Date
+tool.view.duration=Dur&#233;e
+tool.view.actions=Actions
+tool.view.dateFormat=E, MM dd, yyyy HH:mm:ss Z
+
+tool.error.general=Pas possible &#233;tablir la connection.
diff --git a/bbb-lti/grails-app/services/org/bigbluebutton/BigbluebuttonService.groovy b/bbb-lti/grails-app/services/org/bigbluebutton/BigbluebuttonService.groovy
index 297a6ce55422c33579812f4de11ead16c2791793..17714737b5b13d625525ff043bdcfbd90ac3f930 100644
--- a/bbb-lti/grails-app/services/org/bigbluebutton/BigbluebuttonService.groovy
+++ b/bbb-lti/grails-app/services/org/bigbluebutton/BigbluebuttonService.groovy
@@ -34,6 +34,10 @@ import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
 
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.XML;
 import org.w3c.dom.Document;
 import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
@@ -63,20 +67,20 @@ class BigbluebuttonService {
         try {
             docBuilder = docBuilderFactory.newDocumentBuilder()
         } catch (ParserConfigurationException e) {
-            logger.error("Failed to initialise BaseProxy", e)
+            log.error("Failed to initialise BaseProxy", e)
         }
-
         //Instantiate bbbProxy and initialize it with default url and salt
         bbbProxy = new Proxy(url, salt)
     }
 
     public String getJoinURL(params, welcome, mode){
-        //Set the injected values
-        if( !url.equals(bbbProxy.url) && !url.equals("") ) bbbProxy.setUrl(url)
-        if( !salt.equals(bbbProxy.salt) && !salt.equals("") ) bbbProxy.setSalt(salt)
-
-        String joinURL = null
-
+        // Set the injected values
+        if (!url.equals(bbbProxy.url) && !url.equals("")) {
+            bbbProxy.setUrl(url)
+        }
+        if (!salt.equals(bbbProxy.salt) && !salt.equals("")) {
+            bbbProxy.setSalt(salt)
+        }
         String meetingName = getValidatedMeetingName(params.get(Parameter.RESOURCE_LINK_TITLE))
         String meetingID = getValidatedMeetingId(params.get(Parameter.RESOURCE_LINK_ID), params.get(Parameter.CONSUMER_ID))
         String attendeePW = DigestUtils.shaHex("ap" + params.get(Parameter.RESOURCE_LINK_ID) + params.get(Parameter.CONSUMER_ID))
@@ -86,7 +90,6 @@ class BigbluebuttonService {
         String userFullName = getValidatedUserFullName(params, isModerator)
         String courseTitle = getValidatedCourseTitle(params.get(Parameter.COURSE_TITLE))
         String userID = getValidatedUserId(params.get(Parameter.USER_ID))
-
         Integer voiceBridge = 0
         String record = false
         Integer duration = 0
@@ -95,98 +98,93 @@ class BigbluebuttonService {
             record = getValidatedBBBRecord(params.get(Parameter.CUSTOM_RECORD)) || ltiService.allRecordedByDefault()
             duration = getValidatedBBBDuration(params.get(Parameter.CUSTOM_DURATION))
         }
-
         Boolean allModerators = Boolean.valueOf(false)
         if ( params.containsKey(Parameter.CUSTOM_ALL_MODERATORS) ) {
             allModerators = Boolean.parseBoolean(params.get(Parameter.CUSTOM_ALL_MODERATORS))
         }
-
         String[] values = [meetingName, courseTitle]
         String welcomeMsg = MessageFormat.format(welcome, values)
-
         String meta = getMonitoringMetaData(params)
-
-        String createURL = getCreateURL( meetingName, meetingID, attendeePW, moderatorPW, welcomeMsg, voiceBridge, logoutURL, record, duration, meta )
-        log.debug "createURL: " + createURL
-        Map<String, Object> createResponse = doAPICall(createURL)
-        log.debug "createResponse: " + createResponse
-
-        if( createResponse != null){
-            String returnCode = (String) createResponse.get("returncode")
-            String messageKey = (String) createResponse.get("messageKey")
-            if ( Proxy.APIRESPONSE_SUCCESS.equals(returnCode) ||
-                (Proxy.APIRESPONSE_FAILED.equals(returnCode) &&  (Proxy.MESSAGEKEY_IDNOTUNIQUE.equals(messageKey) || Proxy.MESSAGEKEY_DUPLICATEWARNING.equals(messageKey)) ) ){
-                joinURL = bbbProxy.getJoinURL( userFullName, meetingID, (isModerator || allModerators)? moderatorPW: attendeePW, (String) createResponse.get("createTime"), userID);
-            }
+        String createURL = getCreateURL(meetingName, meetingID, attendeePW, moderatorPW, welcomeMsg, voiceBridge, logoutURL, record, duration, meta)
+        Map<String, Object> responseAPICall = doAPICall(createURL)
+        log.info "responseAPICall: " + responseAPICall
+        if (responseAPICall == null) {
+            return null
         }
-
+        Object response = (Object)responseAPICall.get("response")
+        String returnCode = (String)response.get("returncode")
+        String messageKey = (String)response.get("messageKey")
+        if (!Proxy.APIRESPONSE_SUCCESS.equals(returnCode) ||
+            !Proxy.MESSAGEKEY_IDNOTUNIQUE.equals(messageKey) &&
+            !Proxy.MESSAGEKEY_DUPLICATEWARNING.equals(messageKey) &&
+            !"".equals(messageKey)) {
+            return null
+        }
+        def joinURL = bbbProxy.getJoinURL(userFullName, meetingID, (isModerator || allModerators)? moderatorPW: attendeePW, (String) response.get("createTime"), userID)
+        log.info "joinURL: " + joinURL
         return joinURL
     }
 
-    public Object getRecordings(params){
-        //Set the injected values
-        if( !url.equals(bbbProxy.url) && !url.equals("") ) bbbProxy.setUrl(url)
-        if( !salt.equals(bbbProxy.salt) && !salt.equals("") ) bbbProxy.setSalt(salt)
-
+    public Object getRecordings(params) {
+        // Set the injected values
+        if (!url.equals(bbbProxy.url) && !url.equals("")) {
+            bbbProxy.setUrl(url)
+        }
+        if (!salt.equals(bbbProxy.salt) && !salt.equals("")) {
+            bbbProxy.setSalt(salt)
+        }
         String meetingID = getValidatedMeetingId(params.get(Parameter.RESOURCE_LINK_ID), params.get(Parameter.CONSUMER_ID))
-
-        String recordingsURL = bbbProxy.getGetRecordingsURL( meetingID )
-        log.debug "recordingsURL: " + recordingsURL
-        Map<String, Object> recordings = doAPICall(recordingsURL)
-
-        if( recordings != null){
-            String returnCode = (String) recordings.get("returncode")
-            String messageKey = (String) recordings.get("messageKey")
-            if ( Proxy.APIRESPONSE_SUCCESS.equals(returnCode) && messageKey == null ){
-                return recordings.get("recordings")
-            }
+        String recordingsURL = bbbProxy.getGetRecordingsURL(meetingID)
+        Map<String, Object> responseAPICall = doAPICall(recordingsURL)
+        if (responseAPICall == null) {
+            return null
         }
-
-        return null
+        Object response = (Object)responseAPICall.get("response")
+        String returnCode = (String)response.get("returncode")
+        String messageKey = (String)response.get("messageKey")
+        if (!Proxy.APIRESPONSE_SUCCESS.equals(returnCode) || messageKey != null) {
+            return null
+        }
+        Object recordings = (Object)response.get("recordings")
+        return recordings
     }
 
     public Object doDeleteRecordings(params){
-        //Set the injected values
-        if( !url.equals(bbbProxy.url) && !url.equals("") ) bbbProxy.setUrl(url)
-        if( !salt.equals(bbbProxy.salt) && !salt.equals("") ) bbbProxy.setSalt(salt)
-
-        Map<String, Object> result
-
+        // Set the injected values
+        if (!url.equals(bbbProxy.url) && !url.equals("")) {
+            bbbProxy.setUrl(url)
+        }
+        if (!salt.equals(bbbProxy.salt) && !salt.equals("")) {
+            bbbProxy.setSalt(salt)
+        }
         String recordingId = getValidatedBBBRecordingId(params.get(Parameter.BBB_RECORDING_ID))
-
-        if( !recordingId.equals("") ){
+        if (!recordingId.equals("")) {
             String deleteRecordingsURL = bbbProxy.getDeleteRecordingsURL( recordingId )
-            log.debug "deleteRecordingsURL: " + deleteRecordingsURL
-            result = doAPICall(deleteRecordingsURL)
-        } else {
-            result = new HashMap<String, String>()
-            result.put("resultMessageKey", "InvalidRecordingId")
-            result.put("resultMessage", "RecordingId is invalid. The recording can not be deleted.")
+            return doAPICall(deleteRecordingsURL)
         }
-
+        def result = new HashMap<String, String>()
+        result.put("messageKey", "InvalidRecordingId")
+        result.put("message", "RecordingId is invalid. The recording can not be deleted.")
         return result
     }
 
     public Object doPublishRecordings(params){
-        //Set the injected values
-        if( !url.equals(bbbProxy.url) && !url.equals("") ) bbbProxy.setUrl(url)
-        if( !salt.equals(bbbProxy.salt) && !salt.equals("") ) bbbProxy.setSalt(salt)
-
-        Map<String, Object> result
-
+        // Set the injected values
+        if (!url.equals(bbbProxy.url) && !url.equals("")) {
+            bbbProxy.setUrl(url)
+        }
+        if (!salt.equals(bbbProxy.salt) && !salt.equals("")) {
+            bbbProxy.setSalt(salt)
+        }
         String recordingId = getValidatedBBBRecordingId(params.get(Parameter.BBB_RECORDING_ID))
         String publish = getValidatedBBBRecordingPublished(params.get(Parameter.BBB_RECORDING_PUBLISHED))
-
         if( !recordingId.equals("") ){
             String publishRecordingsURL = bbbProxy.getPublishRecordingsURL( recordingId, "true".equals(publish)?"false":"true" )
-            log.debug "publishRecordingsURL: " + publishRecordingsURL
-            result = doAPICall(publishRecordingsURL)
-        } else {
-            result = new HashMap<String, String>()
-            result.put("resultMessageKey", "InvalidRecordingId")
-            result.put("resultMessage", "RecordingId is invalid. The recording can not be deleted.")
+            return doAPICall(publishRecordingsURL)
         }
-
+        def result = new HashMap<String, String>()
+        result.put("messageKey", "InvalidRecordingId")
+        result.put("message", "RecordingId is invalid. The recording can not be deleted.")
         return result
     }
 
@@ -219,14 +217,14 @@ class BigbluebuttonService {
         String userFirstName = params.get(Parameter.USER_FIRSTNAME)
         String userLastName = params.get(Parameter.USER_LASTNAME)
         if( userFullName == null || userFullName == "" ){
-            if( userFirstName != null && userFirstName != "" ){
+            if (userFirstName != null && userFirstName != "") {
                 userFullName = userFirstName
             }
-            if( userLastName != null && userLastName != "" ){
+            if (userLastName != null && userLastName != "") {
                 userFullName += userFullName.length() > 0? " ": ""
                 userFullName += userLastName
             }
-            if( userFullName == null || userFullName == "" ){
+            if (userFullName == null || userFullName == "") {
                 userFullName = isModerator? "Moderator" : "Attendee"
             }
         }
@@ -263,8 +261,7 @@ class BigbluebuttonService {
 
     private String getMonitoringMetaData(params){
         String meta
-
-        meta = "meta_origin=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_CODE) == null? "": params.get(Parameter.TOOL_CONSUMER_CODE))
+        meta  = "meta_origin=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_CODE) == null? "": params.get(Parameter.TOOL_CONSUMER_CODE))
         meta += "&meta_originVersion=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_VERSION) == null? "": params.get(Parameter.TOOL_CONSUMER_VERSION))
         meta += "&meta_originServerCommonName=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_INSTANCE_DESCRIPTION) == null? "": params.get(Parameter.TOOL_CONSUMER_INSTANCE_DESCRIPTION))
         meta += "&meta_originServerUrl=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_INSTANCE_URL) == null? "": params.get(Parameter.TOOL_CONSUMER_INSTANCE_URL))
@@ -272,25 +269,21 @@ class BigbluebuttonService {
         meta += "&meta_contextId=" + bbbProxy.getStringEncoded(params.get(Parameter.COURSE_ID) == null? "": params.get(Parameter.COURSE_ID))
         meta += "&meta_contextActivity=" + bbbProxy.getStringEncoded(params.get(Parameter.RESOURCE_LINK_TITLE) == null? "": params.get(Parameter.RESOURCE_LINK_TITLE))
         meta += "&meta_contextActivityDescription=" + bbbProxy.getStringEncoded(params.get(Parameter.RESOURCE_LINK_DESCRIPTION) == null? "": params.get(Parameter.RESOURCE_LINK_DESCRIPTION))
-
         return meta
     }
 
     /** Make an API call */
     private Map<String, Object> doAPICall(String query) {
         StringBuilder urlStr = new StringBuilder(query);
-
         try {
             // open connection
-            //log.debug("doAPICall.call: " + query );
-
+            log.debug("doAPICall.call: " + query );
             URL url = new URL(urlStr.toString());
             HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
             httpConnection.setUseCaches(false);
             httpConnection.setDoOutput(true);
             httpConnection.setRequestMethod("GET");
             httpConnection.connect();
-
             int responseCode = httpConnection.getResponseCode();
             if (responseCode == HttpURLConnection.HTTP_OK) {
                 // read response
@@ -302,35 +295,27 @@ class BigbluebuttonService {
                     reader = new BufferedReader(isr);
                     String line = reader.readLine();
                     while (line != null) {
-                        if( !line.startsWith("<?xml version=\"1.0\"?>"))
+                        if( !line.startsWith("<?xml version=\"1.0\"?>")) {
                             xml.append(line.trim());
+                        }
                         line = reader.readLine();
                     }
                 } finally {
-                    if (reader != null)
+                    if (reader != null) {
                         reader.close();
-                    if (isr != null)
+                    }
+                    if (isr != null) {
                         isr.close();
+                    }
                 }
                 httpConnection.disconnect();
-
-                // parse response
+                // Parse response.
                 //log.debug("doAPICall.responseXml: " + xml);
                 //Patch to fix the NaN error
                 String stringXml = xml.toString();
                 stringXml = stringXml.replaceAll(">.\\s+?<", "><");
-
-                Document dom = null;
-                dom = docBuilder.parse(new InputSource( new StringReader(stringXml)));
-
-                Map<String, Object> response = getNodesAsMap(dom, "response");
-                //log.debug("doAPICall.responseMap: " + response);
-
-                String returnCode = (String) response.get("returncode");
-                if (Proxy.APIRESPONSE_FAILED.equals(returnCode)) {
-                    log.debug("doAPICall." + (String) response.get("messageKey") + ": Message=" + (String) response.get("message"));
-                }
-
+                JSONObject rootJSON = XML.toJSONObject(stringXml);
+                Map<String, Object> response = jsonToMap(rootJSON);
                 return response;
             } else {
                 log.debug("doAPICall.HTTPERROR: Message=" + "BBB server responded with HTTP status code " + responseCode);
@@ -346,43 +331,43 @@ class BigbluebuttonService {
         }
     }
 
-    /** Get all nodes under the specified element tag name as a Java map */
-    protected Map<String, Object> getNodesAsMap(Document dom, String elementTagName) {
-        Node firstNode = dom.getElementsByTagName(elementTagName).item(0);
-        return processNode(firstNode);
+    protected Map<String, Object> jsonToMap(JSONObject json) throws JSONException {
+        Map<String, Object> retMap = new HashMap<String, Object>();
+        if(json != JSONObject.NULL) {
+            retMap = toMap(json);
+        }
+        return retMap;
     }
 
-    protected Map<String, Object> processNode(Node _node) {
+    protected Map<String, Object> toMap(JSONObject object) throws JSONException {
         Map<String, Object> map = new HashMap<String, Object>();
-        NodeList responseNodes = _node.getChildNodes();
-        for (int i = 0; i < responseNodes.getLength(); i++) {
-            Node node = responseNodes.item(i);
-            String nodeName = node.getNodeName().trim();
-            if (node.getChildNodes().getLength() == 1
-                    && ( node.getChildNodes().item(0).getNodeType() == org.w3c.dom.Node.TEXT_NODE || node.getChildNodes().item(0).getNodeType() == org.w3c.dom.Node.CDATA_SECTION_NODE) ) {
-                String nodeValue = node.getTextContent();
-                map.put(nodeName, nodeValue != null ? nodeValue.trim() : null);
-
-            } else if (node.getChildNodes().getLength() == 0
-                    && node.getNodeType() != org.w3c.dom.Node.TEXT_NODE
-                    && node.getNodeType() != org.w3c.dom.Node.CDATA_SECTION_NODE) {
-                map.put(nodeName, "");
-
-            } else if ( node.getChildNodes().getLength() >= 1
-                    && node.getChildNodes().item(0).getChildNodes().item(0).getNodeType() != org.w3c.dom.Node.TEXT_NODE
-                    && node.getChildNodes().item(0).getChildNodes().item(0).getNodeType() != org.w3c.dom.Node.CDATA_SECTION_NODE ) {
-
-                List<Object> list = new ArrayList<Object>();
-                for (int c = 0; c < node.getChildNodes().getLength(); c++) {
-                    Node n = node.getChildNodes().item(c);
-                    list.add(processNode(n));
-                }
-                map.put(nodeName, list);
-
-            } else {
-                map.put(nodeName, processNode(node));
+        Iterator<String> keysItr = object.keys();
+        while(keysItr.hasNext()) {
+            String key = keysItr.next();
+            Object value = object.get(key);
+            if(value instanceof JSONArray) {
+                value = toList((JSONArray) value);
+            }
+            else if(value instanceof JSONObject) {
+                value = toMap((JSONObject) value);
             }
+            map.put(key, value);
         }
         return map;
     }
+
+    protected List<Object> toList(JSONArray array) throws JSONException {
+        List<Object> list = new ArrayList<Object>();
+        for(int i = 0; i < array.length(); i++) {
+            Object value = array.get(i);
+            if(value instanceof JSONArray) {
+                value = toList((JSONArray) value);
+            }
+            else if(value instanceof JSONObject) {
+                value = toMap((JSONObject) value);
+            }
+            list.add(value);
+        }
+        return list;
+    }
 }
diff --git a/bbb-lti/grails-app/services/org/bigbluebutton/LtiService.groovy b/bbb-lti/grails-app/services/org/bigbluebutton/LtiService.groovy
index c78c127ba0eecd608ccd347761b02c4cb495a86b..2465c8e330e1c949e5d50538ae8eb8f0aaa38fd9 100644
--- a/bbb-lti/grails-app/services/org/bigbluebutton/LtiService.groovy
+++ b/bbb-lti/grails-app/services/org/bigbluebutton/LtiService.groovy
@@ -46,42 +46,37 @@ class LtiService {
 
     private Map<String, String> getConsumer(consumerId) {
         Map<String, String> consumer = null
-
-        if( this.consumerMap.containsKey(consumerId) ){
+        if (this.consumerMap.containsKey(consumerId)) {
             consumer = new HashMap<String, String>()
             consumer.put("key", consumerId);
             consumer.put("secret",  this.consumerMap.get(consumerId))
         }
-
         return consumer
     }
 
-    private void initConsumerMap(){
+    private void initConsumerMap() {
         this.consumerMap = new HashMap<String, String>()
         String[] consumers = this.consumers.split(",")
-        //for( int i=0; i < consumers.length; i++){
-        if ( consumers.length > 0 ){
+        if ( consumers.length > 0 ) {
             int i = 0;
             String[] consumer = consumers[i].split(":")
             if( consumer.length == 2 ){
                 this.consumerMap.put(consumer[0], consumer[1])
             }
         }
-
     }
 
-    public String sign(String sharedSecret, String data) throws Exception
-    {
+    public String sign(String sharedSecret, String data)
+        throws Exception {
         Mac mac = setKey(sharedSecret)
-
         // Signed String must be BASE64 encoded.
         byte[] signBytes = mac.doFinal(data.getBytes("UTF8"));
         String signature = encodeBase64(signBytes);
         return signature;
     }
 
-    private Mac setKey(String sharedSecret) throws Exception
-    {
+    private Mac setKey(String sharedSecret)
+        throws Exception {
         Mac mac = Mac.getInstance("HmacSHA1");
         byte[] keyBytes = sharedSecret.getBytes("UTF8");
         SecretKeySpec signingKey = new SecretKeySpec(keyBytes, "HmacSHA1");
@@ -110,7 +105,6 @@ class LtiService {
 
     def boolean isSSLEnabled(String query) {
         def ssl_enabled = false
-
         log.debug("Pinging SSL connection")
         try {
             // open connection
@@ -122,14 +116,12 @@ class LtiService {
             httpConnection.setRequestMethod("HEAD")
             httpConnection.setConnectTimeout(5000)
             httpConnection.connect()
-
             int responseCode = httpConnection.getResponseCode()
             if (responseCode == HttpURLConnection.HTTP_OK) {
                 ssl_enabled = true
             } else {
                 log.debug("HTTPERROR: Message=" + "BBB server responded with HTTP status code " + responseCode)
             }
-
         } catch(IOException e) {
             log.debug("IOException: Message=" + e.getMessage())
         } catch(IllegalArgumentException e) {
@@ -148,4 +140,8 @@ class LtiService {
     def boolean allRecordedByDefault() {
         return Boolean.parseBoolean(this.recordedByDefault);
     }
+
+    def String getScheme(request) {
+        return request.isSecure() ? "https" : "http"
+    }
 }
diff --git a/bbb-lti/grails-app/views/tool/error.gsp b/bbb-lti/grails-app/views/tool/error.gsp
index e9cc755530f13de18d75ac2315554e6b4db7f9e6..e1442fa84d478f588c0a4b96420ae898ef3d0d3c 100644
--- a/bbb-lti/grails-app/views/tool/error.gsp
+++ b/bbb-lti/grails-app/views/tool/error.gsp
@@ -1,35 +1,35 @@
-<html>
-  <head>
-    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
-    <title>Error</title>
-    <asset:stylesheet src="bootstrap.css"/>
-    <asset:stylesheet src="tool.css"/>
-    <asset:javascript src="jquery.js"/>
-    <asset:javascript src="bootstrap.js"/>
-  </head>
-  <body>
-    <div class="body">
-      <br/><br/>
-      <div class="container">
-      <g:if test="${ (resultMessageKey == 'InvalidEPortfolioUserId')}">
-        <div class="alert alert-warning">
-          ${resultMessage}
-        </div>
-      </g:if>
-      <g:else>
-        <div class="alert alert-danger">
-          <g:message code="tool.error.general" />
-        </div>
-      </g:else>
-      </div>
-    </div>
-    <!-- {
-            "error": {
-                "messageKey": "${resultMessageKey}",
-                "message": "${resultMessage}"
-            }
-        }
-    -->
-    <br/><br/>
-  </body>
+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
+    <title>Error</title>
+    <asset:stylesheet src="bootstrap.css"/>
+    <asset:stylesheet src="tool.css"/>
+    <asset:javascript src="jquery.js"/>
+    <asset:javascript src="bootstrap.js"/>
+  </head>
+  <body>
+    <div class="body">
+      <br/><br/>
+      <div class="container">
+      <g:if test="${ (resultMessageKey == 'InvalidEPortfolioUserId')}">
+        <div class="alert alert-warning">
+          ${resultMessage}
+        </div>
+      </g:if>
+      <g:else>
+        <div class="alert alert-danger">
+          <g:message code="tool.error.general" />
+        </div>
+      </g:else>
+      </div>
+    </div>
+    <!-- {
+            "error": {
+                "messageKey": "${resultMessageKey}",
+                "message": "${resultMessage}"
+            }
+        }
+    -->
+    <br/><br/>
+  </body>
 </html>
\ No newline at end of file
diff --git a/bbb-lti/grails-app/views/tool/index.gsp b/bbb-lti/grails-app/views/tool/index.gsp
index 342e5a52848c26a9013be97c83b51501be5ba544..ca27d71ca15671b92dff02298e30e04ed852dfe9 100644
--- a/bbb-lti/grails-app/views/tool/index.gsp
+++ b/bbb-lti/grails-app/views/tool/index.gsp
@@ -1,77 +1,88 @@
-<html>
-    <head>
-        <title><g:message code="tool.view.title" /></title>
-        <link rel="shortcut icon" href="${assetPath(src: 'favicon.ico')}" type="image/x-icon">
-        <asset:stylesheet src="bootstrap.css"/>
-        <asset:stylesheet src="dataTables.bootstrap.min.css"/>
-        <asset:javascript src="jquery.js"/>
-        <asset:javascript src="jquery.dataTables.min.js"/>
-        <asset:javascript src="dataTables.bootstrap.min.js"/>
-        <asset:javascript src="dataTables.plugin.datetime.js"/>
-        <asset:javascript src="moment-with-locales.min.js"/>
-        <asset:javascript src="bootstrap.js"/>
-        <asset:javascript src="bootstrap-confirmation.min.js"/>
-        <asset:javascript src="tool.js"/>
-    </head>
-    <body>
-        <h1 style="margin-left:20px; text-align: center;"><a title="<g:message code="tool.view.join" />" class="btn btn-primary btn-large" href="${createLink(controller:'tool', action:'join', id: '0')}"><g:message code="tool.view.join" /></a></h1>
-        <br><br>
-        <div class="container">
-        <table id="recordings" class="table table-striped table-bordered dt-responsive" width="100%">
-            <thead>
-                <tr>
-                    <th class="header c0" style="text-align:center;" scope="col"><g:message code="tool.view.recording" /></th>
-                    <th class="header c1" style="text-align:center;" scope="col"><g:message code="tool.view.activity" /></th>
-                    <th class="header c2" style="text-align:center;" scope="col"><g:message code="tool.view.description" /></th>
-                    <th class="header c3" style="text-align:center;" scope="col"><g:message code="tool.view.date" /></th>
-                    <th class="header c4" style="text-align:center;" scope="col"><g:message code="tool.view.duration" /></th>
-                    <g:if test="${ismoderator}">
-                    <th class="header c5 lastcol" style="text-align:center;" scope="col"><g:message code="tool.view.actions" /></th>
-                    </g:if>
-                </tr>
-            </thead>
-            <tbody>
-            <g:each in="${recordingList}" var="r">
-                <g:if test="${ismoderator || r.published == 'true'}">  
-                <tr class="r0 lastrow">
-                    <td class="cell c0" style="text-align:center;">
-                    <g:if test="${r.published == 'true'}">
-                    <g:each in="${r.playback}" var="p">
-                        <a title="<g:message code="tool.view.recording.format.${p.type}" />" target="_new" href="${p.url}"><g:message code="tool.view.recording.format.${p.type}" /></a>&#32;
-                    </g:each>
-                    </g:if>
-                    </td>
-                    <td class="cell c1" style="text-align:center;">${r.name}</td>
-                    <td class="cell c2" style="text-align:center;">${r.metadata.contextactivitydescription}</td>
-                    <td class="cell c3" style="text-align:center;">${r.unixDate}</td>
-                    <td class="cell c4" style="text-align:center;">${r.duration}</td>
-                    <g:if test="${ismoderator}">
-                    <td class="cell c5 lastcol" style="text-align:center;">
-                      <g:if test="${r.published == 'true'}">
-                      <a title="<g:message code="tool.view.recording.unpublish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-close" name="unpublish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
-                      </g:if>
-                      <g:else>
-                      <a title="<g:message code="tool.view.recording.publish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-open" name="publish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
-                      </g:else>
-                      <a title="<g:message code="tool.view.recording.delete" />" class="btn btn-danger btn-sm glyphicon glyphicon-trash" name="delete_recording" value="${r.recordID}"
-                        data-toggle="confirmation"
-                        data-title="<g:message code="tool.view.recording.delete.confirmation.warning" />"
-                        data-content="<g:message code="tool.view.recording.delete.confirmation" />"
-                        data-btn-ok-label="<g:message code="tool.view.recording.delete.confirmation.yes" />"
-                        data-btn-cancel-label="<g:message code="tool.view.recording.delete.confirmation.no" />"
-                        data-placement="left"
-                        href="${createLink(controller:'tool',action:'delete',id: '0')}?bbb_recording_id=${r.recordID}">
-                      </a>
-                    </td>
-                    </g:if>
-                </tr>
-                </g:if>
-            </g:each>
-            </tbody>
-        </table>
-        </div>
-    </body>
-    <g:javascript>
-        var locale = '${params.launch_presentation_locale}';
-    </g:javascript>  
-</html>
\ No newline at end of file
+<html>
+    <head>
+        <title><g:message code="tool.view.title" /></title>
+        <link rel="shortcut icon" href="${assetPath(src: 'favicon.ico')}" type="image/x-icon">
+        <asset:stylesheet src="bootstrap.css"/>
+        <asset:stylesheet src="dataTables.bootstrap.min.css"/>
+        <asset:stylesheet src="tool.css"/>
+        <asset:javascript src="jquery.js"/>
+        <asset:javascript src="jquery.dataTables.min.js"/>
+        <asset:javascript src="dataTables.bootstrap.min.js"/>
+        <asset:javascript src="dataTables.plugin.datetime.js"/>
+        <asset:javascript src="moment-with-locales.min.js"/>
+        <asset:javascript src="bootstrap.js"/>
+        <asset:javascript src="bootstrap-confirmation.min.js"/>
+        <asset:javascript src="tool.js"/>
+    </head>
+    <body>
+        <h1 style="margin-left:20px; text-align: center;"><a title="<g:message code="tool.view.join" />" class="btn btn-primary btn-large" href="${createLink(controller:'tool', action:'join', id: '0')}"><g:message code="tool.view.join" /></a></h1>
+        <br><br>
+        <div class="container">
+        <table id="recordings" class="table table-striped table-bordered dt-responsive" width="100%">
+            <thead>
+                <tr>
+                    <th class="header c0" style="text-align:center;" scope="col"><g:message code="tool.view.recording" /></th>
+                    <th class="header c1" style="text-align:center;" scope="col"><g:message code="tool.view.activity" /></th>
+                    <th class="header c2" style="text-align:center;" scope="col"><g:message code="tool.view.description" /></th>
+                    <th class="header c3" style="text-align:center;" scope="col"><g:message code="tool.view.preview" /></th>
+                    <th class="header c4" style="text-align:center;" scope="col"><g:message code="tool.view.date" /></th>
+                    <th class="header c5" style="text-align:center;" scope="col"><g:message code="tool.view.duration" /></th>
+                    <g:if test="${ismoderator}">
+                    <th class="header c6 lastcol" style="text-align:center;" scope="col"><g:message code="tool.view.actions" /></th>
+                    </g:if>
+                </tr>
+            </thead>
+            <tbody>
+            <g:each in="${recordingList}" var="r">
+                <g:if test="${ismoderator || r.published == 'true'}">
+                <tr class="r0 lastrow">
+                    <td class="cell c0" style="text-align:center;">
+                    <g:if test="${r.published}">
+                        <g:each in="${r.playback}" var="format">
+                            <a title="<g:message code="tool.view.recording.format.${format.getValue().type}" />" target="_new" href="${format.getValue().url}"><g:message code="tool.view.recording.format.${format.getValue().type}" /></a>&#32;
+                        </g:each>
+                    </g:if>
+                    </td>
+                    <td class="cell c1" style="text-align:left;">${r.name}</td>
+                    <td class="cell c2" style="text-align:left;">${r.metadata.contextactivitydescription}</td>
+                    <td class="cell c3" style="text-align:left;">
+                    <g:if test="${r.published}">
+                        <div>
+                        <g:each in="${r.thumbnails}" var="thumbnail">
+                            <img src="${thumbnail.content}" class="thumbnail"></img>
+                        </g:each>
+                        </div>
+                  </g:if>
+                    </td>
+                    <td class="cell c4" style="text-align:left;">${r.unixDate}</td>
+                    <td class="cell c5" style="text-align:right;">${r.duration}</td>
+                    <g:if test="${ismoderator}">
+                    <td class="cell c6 lastcol" style="text-align:center;">
+                      <g:if test="${r.published}">
+                      <a title="<g:message code="tool.view.recording.unpublish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-open" name="unpublish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
+                      </g:if>
+                      <g:else>
+                      <a title="<g:message code="tool.view.recording.publish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-close" name="publish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
+                      </g:else>
+                      <a title="<g:message code="tool.view.recording.delete" />" class="btn btn-danger btn-sm glyphicon glyphicon-trash" name="delete_recording" value="${r.recordID}"
+                        data-toggle="confirmation"
+                        data-title="<g:message code="tool.view.recording.delete.confirmation.warning" />"
+                        data-content="<g:message code="tool.view.recording.delete.confirmation" />"
+                        data-btn-ok-label="<g:message code="tool.view.recording.delete.confirmation.yes" />"
+                        data-btn-cancel-label="<g:message code="tool.view.recording.delete.confirmation.no" />"
+                        data-placement="left"
+                        href="${createLink(controller:'tool',action:'delete',id: '0')}?bbb_recording_id=${r.recordID}">
+                      </a>
+                    </td>
+                    </g:if>
+                </tr>
+                </g:if>
+            </g:each>
+            </tbody>
+        </table>
+        </div>
+    </body>
+    <g:javascript>
+        var locale = '${params.launch_presentation_locale}';
+    </g:javascript>
+</html>
diff --git a/bigbluebutton-client/branding/default/style/css/V2Theme.css b/bigbluebutton-client/branding/default/style/css/V2Theme.css
index 15871ef2ce0bd6b10422d5c1b1295e7dcfbbed67..87a3e5af58bdb6b61b9de6d09b1d31547cf11871 100755
--- a/bigbluebutton-client/branding/default/style/css/V2Theme.css
+++ b/bigbluebutton-client/branding/default/style/css/V2Theme.css
@@ -1171,6 +1171,15 @@ mx|Panel {
 	icon              : Embed(source="assets/swf/v2_skin.swf", symbol="Icon_Trash");
 }
 
+.presentationUploadProgressBarLabel {
+	color                     : #8A9AA7;
+	fontWeight                : "bold";
+	indeterminateMoveInterval : 28;
+	leading                   : 0;
+	paddingTop                : 0;
+	paddingBottom             : 0;
+}
+
 .presentationUploadProgressBar {
 	trackHeight : 12;
 }
diff --git a/bigbluebutton-client/locale/en_US/bbbResources.properties b/bigbluebutton-client/locale/en_US/bbbResources.properties
index 42c789cbed205941d113429190275267227faac7..4b2c0164583c7e302a5a3c6c05b87823d635ec9e 100755
--- a/bigbluebutton-client/locale/en_US/bbbResources.properties
+++ b/bigbluebutton-client/locale/en_US/bbbResources.properties
@@ -66,6 +66,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
@@ -116,9 +117,9 @@ bbb.mainToolbar.recordBtn.toolTip.wontRecord = This session cannot be recorded
 bbb.mainToolbar.recordBtn.confirm.title = Confirm recording
 bbb.mainToolbar.recordBtn.confirm.message.start = Are you sure you want to start recording the session?
 bbb.mainToolbar.recordBtn.confirm.message.stop = Are you sure you want to stop recording the session?
-bbb.mainToolbar.recordBtn..notification.title = Record Notification
-bbb.mainToolbar.recordBtn..notification.message1 = You can record this meeting.
-bbb.mainToolbar.recordBtn..notification.message2 = You must click the Start/Stop Recording button in the title bar to begin/end recording.
+bbb.mainToolbar.recordBtn.notification.title = Record Notification
+bbb.mainToolbar.recordBtn.notification.message1 = You can record this meeting.
+bbb.mainToolbar.recordBtn.notification.message2 = You must click the Start/Stop Recording button in the title bar to begin/end recording.
 bbb.mainToolbar.recordingLabel.recording = (Recording)
 bbb.mainToolbar.recordingLabel.notRecording = Not Recording
 bbb.waitWindow.waitMessage.message = You are a guest, please wait moderator approval.
diff --git a/bigbluebutton-client/resources/prod/BigBlueButton.html b/bigbluebutton-client/resources/prod/BigBlueButton.html
index a799be30e65458465ffedbe1e3d610741d310b9b..ecd8710c8d5107671da8ae4674687292d4098fca 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/core/PopUpUtil.as b/bigbluebutton-client/src/org/bigbluebutton/core/PopUpUtil.as
index e560d654634179bb4b1f2540edf5037ac07ad7cb..a0285e0084a930b66c693bc68b1ae5668a41ba84 100644
--- a/bigbluebutton-client/src/org/bigbluebutton/core/PopUpUtil.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/core/PopUpUtil.as
@@ -19,15 +19,21 @@
 package org.bigbluebutton.core {
 	import flash.display.DisplayObject;
 	import flash.events.KeyboardEvent;
+	import flash.geom.Point;
 	import flash.ui.Keyboard;
 	import flash.utils.Dictionary;
 	import flash.utils.getQualifiedClassName;
 
+	import mx.containers.Panel;
 	import mx.controls.Alert;
+	import mx.core.Application;
 	import mx.core.FlexGlobals;
 	import mx.core.IChildList;
 	import mx.core.IFlexDisplayObject;
 	import mx.core.IUIComponent;
+	import mx.events.FlexEvent;
+	import mx.events.ResizeEvent;
+	import mx.managers.ISystemManager;
 	import mx.managers.PopUpManager;
 	import mx.managers.SystemManager;
 
@@ -42,6 +48,8 @@ package org.bigbluebutton.core {
 
 		private static var popUpDict:Dictionary = new Dictionary(true);
 
+		private static var lockedPositions:Dictionary = new Dictionary(true);
+
 		public static function initAlert():void {
 			Alert.buttonHeight = 30;
 			Alert.buttonWidth = 100;
@@ -49,6 +57,7 @@ package org.bigbluebutton.core {
 			Alert.noLabel = ResourceUtil.getInstance().getString('bbb.alert.no');
 			Alert.okLabel = ResourceUtil.getInstance().getString('bbb.alert.ok');
 			Alert.yesLabel = ResourceUtil.getInstance().getString('bbb.alert.yes');
+			Application(FlexGlobals.topLevelApplication).addEventListener(ResizeEvent.RESIZE, globalResizeListener);
 		}
 
 		public static function createNonModalPopUp(parent:DisplayObject, className:Class, center:Boolean = true):IFlexDisplayObject {
@@ -113,5 +122,39 @@ package org.bigbluebutton.core {
 				removePopUp(event.currentTarget);
 			}
 		}
+
+		private static function globalResizeListener(event:ResizeEvent):void {
+			var systemManager:ISystemManager = Application(FlexGlobals.topLevelApplication).systemManager;
+			for (var i:int = systemManager.numChildren - 1; i > 0; i -= 1) {
+				if (systemManager.getChildAt(i) is Panel && !hasPositionLocked(systemManager.getChildAt(i))) {
+					PopUpManager.centerPopUp(systemManager.getChildAt(i) as IFlexDisplayObject);
+				}
+			}
+		}
+
+		public static function lockPosition(popUp:*, positionFunction:Function = null):void {
+			var resizeHandler:Function = function():void {
+				if (positionFunction != null) {
+					var newPosition:Point = positionFunction.apply();
+					popUp.move(newPosition.x, newPosition.y);
+				}
+			};
+			popUp.addEventListener(FlexEvent.CREATION_COMPLETE, resizeHandler);
+			Application(FlexGlobals.topLevelApplication).addEventListener(ResizeEvent.RESIZE, resizeHandler);
+			lockedPositions[getQualifiedClassName(popUp)] = resizeHandler;
+		}
+
+		public static function unlockPosition(popUp:*):void {
+			var fqcn:String = getQualifiedClassName(popUp);
+			if (lockedPositions[fqcn] != undefined) {
+				popUp.removeEventListener(FlexEvent.CREATION_COMPLETE, lockedPositions[fqcn]);
+				Application(FlexGlobals.topLevelApplication).removeEventListener(ResizeEvent.RESIZE, lockedPositions[fqcn]);
+				delete lockedPositions[fqcn];
+			}
+		}
+
+		public static function hasPositionLocked(popUp:*):Boolean {
+			return lockedPositions[getQualifiedClassName(popUp)] != undefined;
+		}
 	}
 }
diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/model/Meeting.as b/bigbluebutton-client/src/org/bigbluebutton/core/model/Meeting.as
index bcb722efa2e73665e23dcd26bdf6da290c546191..4e830b5aaa34322b5281f48bcc5570cfd31312f8 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/core/model/Meeting.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/core/model/Meeting.as
@@ -17,6 +17,7 @@ package org.bigbluebutton.core.model
     public var webcamsOnlyForModerator:Boolean = false;
     public var metadata:Object = null;
     public var muteOnStart:Boolean = false;
-   
+    public var customLogo:String = "";
+    public var customCopyright:String = "";
   }
 }
\ No newline at end of file
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/model/users/EnterApiResponse.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/EnterApiResponse.as
index 75e82373b018508fcacd6920f7627620b4f10bc4..625e9547fb67b3a18b073dcc512664a10a5202a3 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/EnterApiResponse.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/EnterApiResponse.as
@@ -11,13 +11,14 @@ package org.bigbluebutton.main.model.users
     public var authToken: String;
     public var customdata:Object = new Object();
     public var logoutUrl: String;
-	public var logoutTimer : int;
+    public var logoutTimer : int;
     public var defaultLayout: String;
     public var avatarURL: String;
     public var dialnumber: String;
     public var voiceConf: String;
     public var welcome: String;
-
+    public var customLogo:String;
+    public var customCopyright:String;
     public var meetingName: String;
     public var extMeetingId: String;
     public var intMeetingId: String;
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/JoinService.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/JoinService.as
index 4123ea05060ef2d85a75cc97aee884a0573f1c6c..d1feb7206a601447f65e00385d3c9e771ac72dda 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/JoinService.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/JoinService.as
@@ -158,9 +158,10 @@ package org.bigbluebutton.main.model.users
 
         apiResponse.welcome = result.response.welcome;
         apiResponse.logoutUrl = processLogoutUrl(result.response);
-		apiResponse.logoutTimer = result.response.logoutTimer;
+        apiResponse.logoutTimer = result.response.logoutTimer;
         apiResponse.defaultLayout = result.response.defaultLayout;
-        apiResponse.avatarURL = result.response.avatarURL
+        apiResponse.avatarURL = result.response.avatarURL;
+        
         apiResponse.customdata = new Object();
         
         if (result.response.customdata) {
@@ -178,7 +179,10 @@ package org.bigbluebutton.main.model.users
         if (result.response.hasOwnProperty("modOnlyMessage")) {
           apiResponse.modOnlyMessage = result.response.modOnlyMessage;
         }
-        
+				
+				apiResponse.customLogo = result.response.customLogoURL;
+        apiResponse.customCopyright = result.response.customCopyright;
+				
         if (_resultListener != null) _resultListener(true, apiResponse);
       }
       
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/UserService.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/UserService.as
index 65d13894007e22b47379a62e55c017f212650ada..1c4bbde45d52600941a20a7a7cf4c58d4d9efd64 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/UserService.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/UserService.as
@@ -22,6 +22,7 @@ package org.bigbluebutton.main.model.users
 	
 	import flash.external.ExternalInterface;
 	import flash.net.NetConnection;
+	
 	import org.as3commons.logging.api.ILogger;
 	import org.as3commons.logging.api.getClassLogger;
 	import org.bigbluebutton.core.BBB;
@@ -129,7 +130,8 @@ package org.bigbluebutton.main.model.users
         LiveMeeting.inst().meeting.webcamsOnlyForModerator = result.webcamsOnlyForModerator;
         LiveMeeting.inst().meeting.metadata = result.metadata;
         LiveMeeting.inst().meeting.muteOnStart = meetingOptions.muteOnStart;
-        
+        LiveMeeting.inst().meeting.customLogo = result.customLogo;
+				LiveMeeting.inst().meeting.customCopyright = result.customCopyright;
 				
 				// assign the meeting name to the document title
 				ExternalInterface.call("setTitle", result.meetingName);
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/main/views/CameraDisplaySettings.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/CameraDisplaySettings.mxml
index 5391b2262dcc46be17f37c4d96868eee1e9ed47c..ac0760643d9eca05d64a05f5601a682ab49afff6 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/views/CameraDisplaySettings.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/CameraDisplaySettings.mxml
@@ -60,11 +60,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 		private var selectedCam:int;
 		private var aspectRatio:Number = 1;
 		
-	    override public function move(x:Number, y:Number):void
-	    {
-	       return;
-	    }
-
 		private function onCreationComplete():void {
             tabIndex = 51;
 		}
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/FlashMicSettings.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/FlashMicSettings.mxml
index c5fab8fe03f26bd63ce141e2f284558bc648ecf8..f9b5a59e83620cf995be22a7342430310b553fec 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/views/FlashMicSettings.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/FlashMicSettings.mxml
@@ -89,10 +89,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
       private var my_nc:NetConnection;      
       private var nsStream:NetStream;
       
-			override public function move(x:Number, y:Number):void {
-				return;
-			}
-			
 			private function selectMicrophone(event:Event):void {
 				testMicrophoneLoopback();
 			}
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/GuestWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/GuestWindow.mxml
index 21601df606c3966582ae7d7fe52facfb264d3fce..cfd4db6a71f0fd932198201b77871070460f73f8 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/views/GuestWindow.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/GuestWindow.mxml
@@ -34,17 +34,16 @@ $Id: $
     
     <fx:Script>
         <![CDATA[
-            import com.asfusion.mate.events.Dispatcher;
-            
-            import mx.managers.PopUpManager;
-            
-            import org.bigbluebutton.core.model.LiveMeeting;
-            import org.bigbluebutton.core.model.users.GuestWaiting;
-            import org.bigbluebutton.main.events.BBBEvent;
-            import org.bigbluebutton.main.events.RemoveGuestEvent;
-            import org.bigbluebutton.main.events.RemoveGuestFromViewEvent;
-            import org.bigbluebutton.main.events.ResponseModeratorEvent;
-            import org.bigbluebutton.util.i18n.ResourceUtil;
+			import com.asfusion.mate.events.Dispatcher;
+			
+			import org.bigbluebutton.core.PopUpUtil;
+			import org.bigbluebutton.core.model.LiveMeeting;
+			import org.bigbluebutton.core.model.users.GuestWaiting;
+			import org.bigbluebutton.main.events.BBBEvent;
+			import org.bigbluebutton.main.events.RemoveGuestEvent;
+			import org.bigbluebutton.main.events.RemoveGuestFromViewEvent;
+			import org.bigbluebutton.main.events.ResponseModeratorEvent;
+			import org.bigbluebutton.util.i18n.ResourceUtil;
             
             private var guestButtons:Object = new Object();
             [Bindable] private var numberOfGuests:Number = 0;
@@ -127,7 +126,8 @@ $Id: $
             
             public function closeWindow():void {
                 this.visible = false;
-                PopUpManager.removePopUp(this);
+				PopUpUtil.unlockPosition(this);
+				PopUpUtil.removePopUp(this);
             }
             
         ]]>
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/LockSettings.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/LockSettings.mxml
index 82da5cbe347b1d786a9beebd310ba7221e93c9d4..e143e6ca3c68d165386c014d291dcc5639a7951c 100644
--- a/bigbluebutton-client/src/org/bigbluebutton/main/views/LockSettings.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/LockSettings.mxml
@@ -42,10 +42,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			[Bindable] public var lockOnJoin:Boolean = false;
 			[Bindable] public var lockOnJoinConfigurable:Boolean = false;
 			  
-			override public function move(x:Number, y:Number):void {
-				return;
-			}
-			
 			private function handleKeyDown(event:KeyboardEvent):void {
 				if (event.charCode == Keyboard.ESCAPE) {
 					onCancelClicked();
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/LogoutWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/LogoutWindow.mxml
index 7fbcafb862f8ca76e7c293db655370d10d20caa7..8a86e4a2de30ed45db304fab9e4bd9968d1acaf5 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/views/LogoutWindow.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/LogoutWindow.mxml
@@ -33,16 +33,15 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 
   <fx:Script>
     <![CDATA[
-      import com.asfusion.mate.events.Dispatcher;
-      
-      import mx.managers.PopUpManager;
-      
-      import org.as3commons.logging.api.ILogger;
-      import org.as3commons.logging.api.getClassLogger;
-      import org.bigbluebutton.core.UsersUtil;
-      import org.bigbluebutton.main.events.BBBEvent;
-      import org.bigbluebutton.main.events.LogoutEvent;
-      import org.bigbluebutton.util.i18n.ResourceUtil;
+		import com.asfusion.mate.events.Dispatcher;
+		
+		import org.as3commons.logging.api.ILogger;
+		import org.as3commons.logging.api.getClassLogger;
+		import org.bigbluebutton.core.PopUpUtil;
+		import org.bigbluebutton.core.UsersUtil;
+		import org.bigbluebutton.main.events.BBBEvent;
+		import org.bigbluebutton.main.events.LogoutEvent;
+		import org.bigbluebutton.util.i18n.ResourceUtil;
 
       private static const LOGGER:ILogger = getClassLogger(LogoutWindow);
       
@@ -70,7 +69,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
       }
 
       private function close():void {
-        PopUpManager.removePopUp(this);
+		PopUpUtil.unlockPosition(this);
+        PopUpUtil.removePopUp(this);
       }
     ]]>
   </fx:Script>
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainApplicationShell.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainApplicationShell.mxml
index 6964d327e6d0e696b0634967c07135d4dd729165..4e34c663778fb1348970799d49fad3cefc8d5a7b 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainApplicationShell.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainApplicationShell.mxml
@@ -84,6 +84,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 		<mate:Listener type="{BBBEvent.WAITING_FOR_MODERATOR_ACCEPTANCE}" method="openWaitWindow" />
 		<mate:Listener type="{BBBEvent.RECONNECT_DISCONNECTED_EVENT}" method="closeWaitWindow"/>
     	<mate:Listener type="{RoundTripLatencyReceivedEvent.ROUND_TRIP_LATENCY_RECEIVED}" method="handleRoundTripLatencyReceivedEvent"/>
+		<mate:Listener type="{ConferenceCreatedEvent.CONFERENCE_CREATED_EVENT}" method="handleConferenceCreatedEvent" />
     
 	</fx:Declarations>
 	<fx:Script>
@@ -125,6 +126,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			import org.bigbluebutton.core.events.NewGuestWaitingEvent;
 			import org.bigbluebutton.core.events.RoundTripLatencyReceivedEvent;
 			import org.bigbluebutton.core.events.SwitchedLayoutEvent;
+			import org.bigbluebutton.core.model.LiveMeeting;
 			import org.bigbluebutton.core.vo.LockSettingsVO;
 			import org.bigbluebutton.main.events.AppVersionEvent;
 			import org.bigbluebutton.main.events.BBBEvent;
@@ -143,6 +145,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			import org.bigbluebutton.main.model.options.BrowserVersionsOptions;
 			import org.bigbluebutton.main.model.options.LanguageOptions;
 			import org.bigbluebutton.main.model.options.LayoutOptions;
+			import org.bigbluebutton.main.model.users.events.ConferenceCreatedEvent;
 			import org.bigbluebutton.main.model.users.events.ConnectionFailedEvent;
 			import org.bigbluebutton.modules.phone.events.AudioSelectionWindowEvent;
 			import org.bigbluebutton.modules.phone.events.FlashMicSettingsEvent;
@@ -245,12 +248,20 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 				_respTimer.start();
 			}
 			
+			private function handleConferenceCreatedEvent(event:ConferenceCreatedEvent):void {
+				updateCopyrightText();
+			}
+			
 			private function updateCopyrightText():void {
 				if (StringUtils.isEmpty(brandingOptions.copyright)) {
 					copyrightText = ResourceUtil.getInstance().getString('bbb.mainshell.copyrightLabel2',[appVersion]);
 				} else {
 					copyrightText = String(brandingOptions.copyright).replace("{0}", appVersion);
 				}
+				
+				if (!StringUtils.isEmpty(LiveMeeting.inst().meeting.customCopyright)) {
+					copyrightText = LiveMeeting.inst().meeting.customCopyright;
+				}
 			}
 			
 			private function initQuote() : void {
@@ -400,11 +411,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 				}
 
 				if (guestWindow == null) {
-					guestWindow = PopUpUtil.createModalPopUp( mdiCanvas, GuestWindow, false) as GuestWindow;
+					guestWindow = PopUpUtil.createModalPopUp( FlexGlobals.topLevelApplication as DisplayObject, GuestWindow, false) as GuestWindow;
+					
+					PopUpUtil.lockPosition(guestWindow, function():Point {
+						return new Point(systemManager.screen.width - guestWindow.width - 20, 20);
+					});
+					
 					guestWindow.addEventListener(Event.CLOSE, closeGuestWindow);
-
-					guestWindow.x = systemManager.screen.width - guestWindow.width - 20;
-					guestWindow.y = 20;
 				}
 				guestWindow.refreshGuestView();
 			}
@@ -424,11 +437,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 
 			private function openWaitWindow(evt:BBBEvent):void {
 				progressBar.visible = false;
-				waitWindow = PopUpUtil.createModalPopUp( mdiCanvas, WaitingWindow, false) as WaitingWindow;
-
-				// Calculate position of TitleWindow in Application's coordinates.
-				waitWindow.x = (systemManager.screen.width - waitWindow.width) / 2;
-				waitWindow.y = (systemManager.screen.height - waitWindow.height) / 2;
+				waitWindow = PopUpUtil.createModalPopUp( FlexGlobals.topLevelApplication as DisplayObject, WaitingWindow, true) as WaitingWindow;
 			}
 			
 			private function openLogWindow():void {
@@ -562,7 +571,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 	   		}
 
 			private function handleInactivityWarningEvent(e:BBBEvent):void {
-				var inactivityWarning:InactivityWarningWindow = PopUpUtil.createModalPopUp(mdiCanvas, InactivityWarningWindow, true) as InactivityWarningWindow;
+				var inactivityWarning:InactivityWarningWindow = PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, InactivityWarningWindow, true) as InactivityWarningWindow;
 				inactivityWarning.duration = e.payload.duration;
 			}
 
@@ -573,52 +582,27 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
                  * avoid this case. - Chad
                  */
                 this.callLater(function():void {
-                    var micSettings:FlashMicSettings = PopUpUtil.createModalPopUp(mdiCanvas, FlashMicSettings, false) as FlashMicSettings;
-                    if (micSettings) {
-                        micSettings.addEventListener(FlexEvent.CREATION_COMPLETE, function(e:Event):void {
-                            var point1:Point = new Point();
-                            // Calculate position of TitleWindow in Application's coordinates. 
-                            point1.x = width / 2;
-                            point1.y = height / 2;
-                            micSettings.x = point1.x - (micSettings.width / 2);
-                            micSettings.y = point1.y - (micSettings.height / 2);
-                        });
-                    }
+                    PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, FlashMicSettings, true) as FlashMicSettings;
                 });
             }
 
             private function openVideoPreviewWindow(event:BBBEvent):void {
-                var camSettings:CameraDisplaySettings = PopUpUtil.createModalPopUp(mdiCanvas, CameraDisplaySettings, true) as CameraDisplaySettings;
+                var camSettings:CameraDisplaySettings = PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, CameraDisplaySettings, true) as CameraDisplaySettings;
                 if (camSettings) {
                     camSettings.defaultCamera = event.payload.defaultCamera;
                     camSettings.camerasArray = event.payload.camerasArray;
                     camSettings.publishInClient = event.payload.publishInClient;
                     camSettings.chromePermissionDenied = event.payload.chromePermissionDenied;
 					camSettings.updateCameraList();
-
-                    var point1:Point = new Point();
-                    // Calculate position of TitleWindow in Application's coordinates. 
-                    point1.x = width / 2;
-                    point1.y = height / 2;
-                    camSettings.x = point1.x - (camSettings.width / 2);
-                    camSettings.y = point1.y - (camSettings.height / 2);
                 }
             }
 
             private function wrongLocaleVersion():void {
-                var localeWindow:OldLocaleWarnWindow = PopUpUtil.createNonModalPopUp(mdiCanvas, OldLocaleWarnWindow, false) as OldLocaleWarnWindow;
-                if (localeWindow) {
-                    var point1:Point = new Point();
-                    // Calculate position of TitleWindow in Application's coordinates. 
-                    point1.x = width / 2;
-                    point1.y = height / 2;
-                    localeWindow.x = point1.x - (localeWindow.width / 2);
-                    localeWindow.y = point1.y - (localeWindow.height / 2);
-                }
+                PopUpUtil.createNonModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, OldLocaleWarnWindow, true) as OldLocaleWarnWindow;
             }
 
             private function handleShowAudioSelectionWindowEvent(event:AudioSelectionWindowEvent):void {
-                PopUpUtil.createModalPopUp(mdiCanvas, AudioSelectionWindow, true);
+                PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, AudioSelectionWindow, true);
             }
 
             private function handleRoundTripLatencyReceivedEvent(event: RoundTripLatencyReceivedEvent): void {
@@ -628,7 +612,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
             private function handleWebRTCMediaRequestEvent(event:WebRTCMediaEvent):void {
                 var options:PhoneOptions = new PhoneOptions();
                 if (!options.showMicrophoneHint) return;
-                var browserPermissionHelper:BrowserPermissionHelper = PopUpUtil.createModalPopUp(mdiCanvas, BrowserPermissionHelper, false) as BrowserPermissionHelper;
+                var browserPermissionHelper:BrowserPermissionHelper = PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, BrowserPermissionHelper, false) as BrowserPermissionHelper;
                 if (BrowserCheck.isFirefox()) {
                     if (browserPermissionHelper) {
 						if (Capabilities.os.indexOf("Mac") >= 0){
@@ -650,17 +634,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
             }
 
             private function handleWebRTCEchoTestConnectingEvent(event:WebRTCEchoTestEvent):void {
-                var webRTCEchoTest:WebRTCEchoTest = PopUpUtil.createModalPopUp(mdiCanvas, WebRTCEchoTest, false) as WebRTCEchoTest;
-                if (webRTCEchoTest) {
-                    webRTCEchoTest.addEventListener(FlexEvent.CREATION_COMPLETE, function(e:Event):void {
-                        var point1:Point = new Point();
-                        // Calculate position of TitleWindow in Application's coordinates. 
-                        point1.x = width / 2;
-                        point1.y = height / 2;
-                        webRTCEchoTest.x = point1.x - (webRTCEchoTest.width / 2);
-                        webRTCEchoTest.y = point1.y - (webRTCEchoTest.height / 2);
-                    });
-                }
+                PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, WebRTCEchoTest, true) as WebRTCEchoTest;
             }
 
             private function handleShareCameraRequestEvent(event:ShareCameraRequestEvent):void {
@@ -671,7 +645,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
             }
 			
 			private function showbrowserPermissionHelper() : void {
-				var browserPermissionHelper : BrowserPermissionHelper = PopUpUtil.createNonModalPopUp(mdiCanvas, BrowserPermissionHelper, false) as BrowserPermissionHelper;
+				var browserPermissionHelper : BrowserPermissionHelper = PopUpUtil.createNonModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, BrowserPermissionHelper, false) as BrowserPermissionHelper;
 				if (browserPermissionHelper) {
 					browserPermissionHelper.currentState = "chromeCam";
 					browserPermissionHelper.x = 20;
@@ -689,15 +663,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
                         handleExitApplicationEvent();
                         return;
                     }
-                    var loggedOutWindow:LoggedOutWindow = PopUpUtil.createModalPopUp(mdiCanvas, LoggedOutWindow, false) as LoggedOutWindow;
+                    var loggedOutWindow:LoggedOutWindow = PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, LoggedOutWindow, true) as LoggedOutWindow;
                     if (loggedOutWindow) {
-                        var point1:Point = new Point();
-                        // Calculate position of TitleWindow in Application's coordinates. 
-                        point1.x = width / 2;
-                        point1.y = height / 2;
-                        loggedOutWindow.x = point1.x - (loggedOutWindow.width / 2);
-                        loggedOutWindow.y = point1.y - (loggedOutWindow.height / 2);
-
                         loggedOutWindow.setReason(reason);
                         mdiCanvas.removeAllPopUps();
                         removeToolBars();
@@ -852,7 +819,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
             private function openLockSettingsWindow(event:LockControlEvent):void {
                 var lsv:LockSettingsVO = UsersUtil.getLockSettings();
 
-                var popUp:IFlexDisplayObject = PopUpUtil.createModalPopUp(mdiCanvas, LockSettings, false);
+                var popUp:IFlexDisplayObject = PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, LockSettings, true);
                 if (popUp) {
                     var ls:LockSettings = LockSettings(popUp);
                     ls.disableCam = lsv.getDisableCam();
@@ -862,17 +829,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
                     ls.lockedLayout = lsv.getLockedLayout();
                     ls.lockOnJoin = lsv.getLockOnJoin();
                     ls.lockOnJoinConfigurable = lsv.getLockOnJoinConfigurable();
-
-                    var point1:Point = new Point();
-                    point1.x = width / 2;
-                    point1.y = height / 2;
-                    ls.x = point1.x - (ls.width / 2);
-                    ls.y = point1.y - (ls.height / 2);
                 }
             }
 
             private function openBreakoutRoomsWindow(e:BreakoutRoomEvent):void {
-                var popUp:IFlexDisplayObject = PopUpUtil.createModalPopUp(mdiCanvas, BreakoutRoomSettings, true);
+                var popUp:IFlexDisplayObject = PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, BreakoutRoomSettings, true);
                 if (popUp != null) {
                     BreakoutRoomSettings(popUp).initCreateBreakoutRooms(e.joinMode, e.record);
                 }
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml
index 86fef8d16f6d2688bb6a8d3118226bc42e83cfeb..d6c11afce1b086673d775adbc29c97017face697 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml
@@ -211,6 +211,12 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 					}
 				}
 
+				var customLogo: String = LiveMeeting.inst().meeting.customLogo;
+				
+				if (customLogo != "") {
+					logo.source = LiveMeeting.inst().meeting.customLogo;
+				}
+				
 				if (UsersUtil.isBreakout()) {
 					breakoutRibbon.visible = breakoutRibbon.includeInLayout = true;
 					var sequence:String = StringUtils.substringAfterLast(UsersUtil.getMeetingName(), " ");
@@ -301,13 +307,12 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			
 			private function confirmLogout():void {
 				if (toolbarOptions.confirmLogout) {
-					var logoutWindow:LogoutWindow = PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, LogoutWindow, true) as LogoutWindow;
-
-					var newX:Number = this.width - logoutWindow.width - 5;
-					var newY:Number = btnLogout.y + btnLogout.height + 5;
+					var logoutWindow:LogoutWindow = PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, LogoutWindow, false) as LogoutWindow;
 
-					logoutWindow.x = newX;
-					logoutWindow.y = newY;
+					// Needs to be improved in case of RTL layout
+					PopUpUtil.lockPosition(logoutWindow, function():Point {
+						return new Point(width - logoutWindow.width - 5, btnLogout.y + btnLogout.height + 5)
+					});
 				} else {
 					doLogout();
 				}
@@ -515,7 +520,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			}
 
 			private function onSettingsButtonClick():void {
-				settingsPopup = PopUpUtil.createModalPopUp(this.parent, BBBSettings, true) as BBBSettings;
+				settingsPopup = PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, BBBSettings, true) as BBBSettings;
 				settingsPopup.pushComponents(settingsComponents);
 				PopUpManager.centerPopUp(settingsPopup);
 			}
@@ -548,7 +553,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			<mx:HBox id="titleBox" width="40%" horizontalAlign="left" verticalAlign="middle" horizontalScrollPolicy="off">
 				<mx:Image id="logo" right="20" maxHeight="35" ioError="hideLogo()" />
 				<mx:VRule id="logoSperatator" styleName="toolbarSeparator" height="10" />
-				<mx:Label id="meetingNameLbl" minWidth="1" styleName="meetingNameLabelStyle" width="100%" truncateToFit="true"/>
+				<mx:Label id="meetingNameLbl" minWidth="1" maxWidth="{titleBox.width - logo.width - 20}" styleName="meetingNameLabelStyle" truncateToFit="true"/>
 			</mx:HBox>
 			<mx:HBox id="actionBox" width="30%" horizontalAlign="center" verticalAlign="middle" horizontalScrollPolicy="off">
 				<mx:HBox id="addedBtnsMicrophone" />
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/RecordButton.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/RecordButton.mxml
index 411e63027c39759e810ab156999aeb4977e19e59..7267bde7a5ffa1dde887a9c4cedfe8bf126e392f 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/views/RecordButton.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/RecordButton.mxml
@@ -42,26 +42,27 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 	</fx:Declarations>
 	<fx:Script>
 		<![CDATA[
-      import com.asfusion.mate.events.Dispatcher;
-      
-      import flash.net.navigateToURL;
-      
-      import mx.controls.Alert;
-      import mx.events.CloseEvent;
-      import mx.managers.PopUpManager;
-      
-      import org.as3commons.logging.api.ILogger;
-      import org.as3commons.logging.api.getClassLogger;
-      import org.bigbluebutton.core.BBB;
-      import org.bigbluebutton.core.Options;
-      import org.bigbluebutton.core.UsersUtil;
-      import org.bigbluebutton.core.model.LiveMeeting;
-      import org.bigbluebutton.main.events.BBBEvent;
-      import org.bigbluebutton.main.model.options.LayoutOptions;
-      import org.bigbluebutton.main.model.users.events.ChangeMyRole;
-      import org.bigbluebutton.modules.phone.events.FlashJoinedVoiceConferenceEvent;
-      import org.bigbluebutton.modules.phone.events.WebRTCCallEvent;
-      import org.bigbluebutton.util.i18n.ResourceUtil;
+			import com.asfusion.mate.events.Dispatcher;
+			
+			import flash.net.navigateToURL;
+			
+			import mx.controls.Alert;
+			import mx.events.CloseEvent;
+			import mx.managers.PopUpManager;
+			
+			import org.as3commons.logging.api.ILogger;
+			import org.as3commons.logging.api.getClassLogger;
+			import org.bigbluebutton.core.BBB;
+			import org.bigbluebutton.core.Options;
+			import org.bigbluebutton.core.PopUpUtil;
+			import org.bigbluebutton.core.UsersUtil;
+			import org.bigbluebutton.core.model.LiveMeeting;
+			import org.bigbluebutton.main.events.BBBEvent;
+			import org.bigbluebutton.main.model.options.LayoutOptions;
+			import org.bigbluebutton.main.model.users.events.ChangeMyRole;
+			import org.bigbluebutton.modules.phone.events.FlashJoinedVoiceConferenceEvent;
+			import org.bigbluebutton.modules.phone.events.WebRTCCallEvent;
+			import org.bigbluebutton.util.i18n.ResourceUtil;
 
 			private static const LOGGER:ILogger = getClassLogger(RecordButton);
 
@@ -76,9 +77,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 
 			private function hideConfirmationAlert():void {
 				if (_confirmationAlert != null) {
-					if (_confirmationAlert.visible) {
-						PopUpManager.removePopUp(_confirmationAlert);
-					}
+					PopUpUtil.unlockPosition(_confirmationAlert);
 					_confirmationAlert = null;
 				}
 			}
@@ -91,13 +90,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 				// Confirm logout using built-in alert
 				_confirmationAlert = Alert.show(message, ResourceUtil.getInstance().getString('bbb.mainToolbar.recordBtn.confirm.title'), Alert.YES | Alert.NO, this, onCloseConfirmationDialog, null, Alert.YES);
 
-        // Reach out to MainAppShell to set position of alert window.
-				var newX:Number = this.parent.parent.width * 0.7;
-				var newY:Number = this.y + this.height + 5;
-
-				_confirmationAlert.validateNow();
-				_confirmationAlert.move(newX, newY);
-				//Accessibility.updateProperties();
+				PopUpUtil.lockPosition(_confirmationAlert, function():Point {
+					return new Point(parentApplication.width - 150 - _confirmationAlert.width, y + height + 5); 
+				});
 			}
 
 			private function confirmChangeRecordingStatus():void {
@@ -110,11 +105,12 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			}
 
 			private function onCloseConfirmationDialog(e:CloseEvent):void {
-				hideConfirmationAlert();
 				// check to see if the YES button was pressed
 				if (e.detail==Alert.YES) {
 					 doChangeRecordingStatus();
 				}
+				
+				hideConfirmationAlert();
 			}
 
 			private function doChangeRecordingStatus():void {
@@ -177,7 +173,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 						&& UsersUtil.amIModerator()
 						&& LiveMeeting.inst().meeting.allowStartStopRecording
 						&& this.enabled) {
-					var alert:Alert = Alert.show(ResourceUtil.getInstance().getString("bbb.mainToolbar.recordBtn..notification.message1") + "\n\n" + ResourceUtil.getInstance().getString("bbb.mainToolbar.recordBtn..notification.message2"), ResourceUtil.getInstance().getString("bbb.mainToolbar.recordBtn..notification.title"), Alert.OK, this);
+					var alert:Alert = Alert.show(ResourceUtil.getInstance().getString("bbb.mainToolbar.recordBtn.notification.message1") + "\n\n" + ResourceUtil.getInstance().getString("bbb.mainToolbar.recordBtn.notification.message2"), ResourceUtil.getInstance().getString("bbb.mainToolbar.recordBtn.notification.title"), Alert.OK, this);
 					// we need to set transparency duration to avoid the blur effect when two alerts are displayed sequentially
 					alert.setStyle("modalTransparencyDuration", 250);
 					alert.titleIcon = getStyle('iconRecordReminder');
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/WaitingWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/WaitingWindow.mxml
index d41efed188912e13e529d9b821c5b24ef5526466..f5dc848246246923c13d904f7e1409163e93b4bc 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/views/WaitingWindow.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/WaitingWindow.mxml
@@ -27,12 +27,12 @@
 
 	<fx:Script>
 		<![CDATA[
-			import mx.managers.PopUpManager;
-
+			import org.bigbluebutton.core.PopUpUtil;
 			import org.bigbluebutton.util.i18n.ResourceUtil;
 
 			public function removeWindow():void {
-				PopUpManager.removePopUp(this);
+				PopUpUtil.unlockPosition(this);
+				PopUpUtil.removePopUp(this);
 			}
 		]]>
 	</fx:Script>
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/WebRTCEchoTest.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/WebRTCEchoTest.mxml
index 2e31a4454a1a2f797926061f4603f674905f85b6..e0d4a88492213a237d49f7e0473a69a3f1648e37 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/views/WebRTCEchoTest.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/WebRTCEchoTest.mxml
@@ -71,10 +71,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 
 			private var userClosed:Boolean = false;
 			
-			override public function move(x:Number, y:Number):void {
-				return;
-			}
-			
 			private function onCancelClicked():void {
 				stopTimers();
 				PopUpUtil.removePopUp(this);
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatBox.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatBox.mxml
index 048d7403670e100f23c78157916b53632e83e901..a642ffbd6ac27076e16e6a8062447596f9c39953 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatBox.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatBox.mxml
@@ -463,27 +463,34 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
           sendStartCustomPollEvent(answers);
         } else {
           var publicEvent:SendPublicChatMessageEvent = new SendPublicChatMessageEvent(SendPublicChatMessageEvent.SEND_PUBLIC_CHAT_MESSAGE_EVENT);
-          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();
+          var cm:ChatMessageVO = processChatMessage(message);
           
-          //cm.message = ChatUtil.parseURLs(ChatUtil.cleanup(message));
-          cm.message = ExternalInterface.call('parseURLs', (ChatUtil.cleanup(message)));
-          
-          publicEvent.chatMessage = cm;
-          globalDispatcher.dispatchEvent(publicEvent);			
+          if (cm != null) {
+            publicEvent.chatMessage = cm;
+            globalDispatcher.dispatchEvent(publicEvent);
+          }
         }
       }
       
       private function sendPrivateChatMessage(message:String):void {
         var privateEvent:SendPrivateChatMessageEvent = new SendPrivateChatMessageEvent(SendPrivateChatMessageEvent.SEND_PRIVATE_CHAT_MESSAGE_EVENT);
+        var cm:ChatMessageVO = processChatMessage(message);
+        
+        if (cm != null) {
+          cm.toUserId = chatWithUserID;
+          cm.toUsername = chatWithUsername;
+          
+          privateEvent.chatMessage = cm;
+          globalDispatcher.dispatchEvent(privateEvent);
+		}
+      }
+      
+      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();
@@ -497,12 +504,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
         cm.fromTimezoneOffset = now.getTimezoneOffset();
         
         //cm.message = ChatUtil.parseURLs(ChatUtil.cleanup(message));
-        cm.message = ExternalInterface.call('parseURLs', (ChatUtil.cleanup(message)));
-        cm.toUserId = chatWithUserID;
-        cm.toUsername = chatWithUsername;
+        cm.message = sanitizedMessage;
         
-        privateEvent.chatMessage = cm;
-        globalDispatcher.dispatchEvent(privateEvent);
+        return cm;
       }
       
       private function handleTextInput(e:TextEvent):void {
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 9d9b24c56044a539fd1788e3211c6243205586d9..406ad42aa63fc9ef3fddf3599d2da3664117c001 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/PollDataProcessor.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/PollDataProcessor.as
@@ -99,7 +99,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 222545f335967e917385f6803a79da044049636a..228bbf0656c32cb4d49431bad1a2ace39b75fe2a 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/PollingService.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/service/PollingService.as
@@ -91,7 +91,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/present/managers/PresentManager.as b/bigbluebutton-client/src/org/bigbluebutton/modules/present/managers/PresentManager.as
index 3cfc7d69450a5484d0f0aa2eeff038ac8be4af70..6edad2f68b7a3ff49f7a5ec0354978edd247a91d 100644
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/present/managers/PresentManager.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/managers/PresentManager.as
@@ -21,7 +21,6 @@ package org.bigbluebutton.modules.present.managers
 	import com.asfusion.mate.events.Dispatcher;
 	
 	import flash.display.DisplayObject;
-	import flash.geom.Point;
 	
 	import mx.core.FlexGlobals;
 	
@@ -70,16 +69,10 @@ package org.bigbluebutton.modules.present.managers
 		}
 
 		public function handleOpenUploadWindow(e:UploadEvent):void{
+			// Never use "center" true with FileUploadWindow
 			var uploadWindow : FileUploadWindow = PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, FileUploadWindow, false) as FileUploadWindow;
 			if (uploadWindow) {
 				uploadWindow.maxFileSize = e.maxFileSize;
-				
-				var point1:Point = new Point();
-				point1.x = FlexGlobals.topLevelApplication.width / 2;
-				point1.y = FlexGlobals.topLevelApplication.height / 2;  
-				
-				uploadWindow.x = point1.x - (uploadWindow.width/2);
-				uploadWindow.y = point1.y - (uploadWindow.height/2);
 			}
 		}
 		
@@ -88,15 +81,7 @@ package org.bigbluebutton.modules.present.managers
 		}
 
 		public function handleOpenDownloadWindow():void {
-			var downloadWindow:FileDownloadWindow = PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, FileDownloadWindow, false) as FileDownloadWindow;
-			if (downloadWindow) {
-				var point1:Point = new Point();
-				point1.x = FlexGlobals.topLevelApplication.width / 2;
-				point1.y = FlexGlobals.topLevelApplication.height / 2;
-
-				downloadWindow.x = point1.x - (downloadWindow.width/2);
-				downloadWindow.y = point1.y - (downloadWindow.height/2);
-			}
+			PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, FileDownloadWindow, true) as FileDownloadWindow;
 		}
 
 		public function handleCloseDownloadWindow():void {
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/FileDownloadWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/FileDownloadWindow.mxml
index 8d41b611f7588dfd3135ae0a3916c9b605f55975..774f56b250177ecd8fcfe4ccc2bd63e54a70877b 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/FileDownloadWindow.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/FileDownloadWindow.mxml
@@ -52,10 +52,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 				closeButton.setFocus()
 			}
 
-			override public function move(x:Number, y:Number):void {
-				return;
-			}
-
 			private function initData():void {
 				downloadablePresentations = PresentationModel.getInstance().getDownloadablePresentations();
 			}
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/FileUploadWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/FileUploadWindow.mxml
index 018694f8c8ff1683ea7f073204aad4b158a260b5..17e64eeb3d83d43329582680a074c254e0dfb02d 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/FileUploadWindow.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/FileUploadWindow.mxml
@@ -60,6 +60,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 		import mx.core.mx_internal;
 		import mx.events.FlexEvent;
 		import mx.events.ListEvent;
+		import mx.managers.PopUpManager;
 		import mx.utils.StringUtil;
 		
 		import flashx.textLayout.formats.Direction;
@@ -109,14 +110,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
       private var fileToUpload:FileReference = new FileReference();
       [Bindable] private var presentOptions:PresentOptions;
 		
-      override public function move(x:Number, y:Number):void{
-        return;
-      }
-
 		protected function onCreationComplete(event:FlexEvent):void
 		{
 			dispatcher = new Dispatcher();
 			updateStyles();
+			
+			// For an unknown reason this popup cannot be centred directly using PopUpUtil
+			PopUpManager.centerPopUp(this)
 		}
 		
 		private function updateStyles() : void {
@@ -189,6 +189,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 
         progressReportBox.visible = true;
         progressBar.visible = true;
+		progressBarLabel.visible = true;
         
         disableClosing();
         
@@ -243,23 +244,23 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
       }
 
       private function handleOfficeDocumentConversionFailed(e:OfficeDocConvertFailedEvent):void {
-        var logData:Object = UsersUtil.initLogData();
-        logData.tags = ["presentation-conversion"];
-        logData.filename = fileToUpload.name;
-        logData.message = "Presentation conversion failed";
-        LOGGER.warn(JSON.stringify(logData));
-
+        var logData:Object = UsersUtil.initLogData();
+        logData.tags = ["presentation-conversion"];
+        logData.filename = fileToUpload.name;
+        logData.message = "Presentation conversion failed";
+        LOGGER.warn(JSON.stringify(logData));
+
         enableClosing();
         displayAlert(ResourceUtil.getInstance().getString('bbb.presentation.error.document.convert.failed'));
       }
-
+
       private function handleOfficeDocumentConversionInvalid(e:OfficeDocConvertInvalidEvent):void {
-        var logData:Object = UsersUtil.initLogData();
-        logData.tags = ["presentation-conversion"];
-        logData.filename = fileToUpload.name;
-        logData.message = "Presentation conversion invalid";
-        LOGGER.warn(JSON.stringify(logData));
-
+        var logData:Object = UsersUtil.initLogData();
+        logData.tags = ["presentation-conversion"];
+        logData.filename = fileToUpload.name;
+        logData.message = "Presentation conversion invalid";
+        LOGGER.warn(JSON.stringify(logData));
+
         enableClosing();
         displayAlert(ResourceUtil.getInstance().getString('bbb.presentation.error.document.convert.failed'));
       }
@@ -319,7 +320,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
       }
 		
 	  private function buildProgressLabel(value:String) : void {
-		  progressBar.label = ResourceUtil.getInstance().getString('bbb.fileupload.progBarLbl') + " " + value;
+		  progressBarLabel.text = ResourceUtil.getInstance().getString('bbb.fileupload.progBarLbl') + " " + value;
 	  }
 
       private function handleConversionCompleted(e:ConversionCompletedEvent):void{
@@ -380,7 +381,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 		<s:State name="error" />
 	</mx:states>
 	
-
+
   <mx:VBox width="100%" height="100%" verticalAlign="top" horizontalAlign="center" top="15">
     <common:AdvancedLabel text="{ResourceUtil.getInstance().getString('bbb.fileupload.title')}"
 						  styleName="titleWindowStyle"
@@ -407,11 +408,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 				 toolTip="{ResourceUtil.getInstance().getString('bbb.fileupload.letUserDownload.tooltip')}"
 				 paddingLeft="5" paddingTop="5" visible="{presentOptions.enableDownload}" includeInLayout="{presentOptions.enableDownload}"/>
 	  
-    <mx:HBox id="progressReportBox" width="100%" paddingLeft="10" paddingRight="10" paddingTop="5" paddingBottom="5" verticalAlign="middle"
+    <mx:VBox id="progressReportBox" width="100%" paddingLeft="10" paddingRight="10" paddingTop="5" paddingBottom="5" verticalAlign="middle"
 			 includeInLayout="true" visible="false" excludeFrom="error">
-      <mx:ProgressBar id="progressBar" mode="manual" label="{ResourceUtil.getInstance().getString('bbb.fileupload.progBarLbl')}"
-                      styleName="presentationUploadProgressBar" labelPlacement="top" width="100%" visible="false"/>
-    </mx:HBox>
+	  <mx:Text id="progressBarLabel" styleName="presentationUploadProgressBarLabel"
+			   visible="{progressBar.visible}" width="100%" text="{ResourceUtil.getInstance().getString('bbb.fileupload.progBarLbl')}" />
+      <mx:ProgressBar id="progressBar" mode="manual"
+                      styleName="presentationUploadProgressBar" width="100%" visible="false"/>
+    </mx:VBox>
 	
 	<mx:VBox id="errorBox" paddingTop="5" paddingBottom="5" width="100%" horizontalAlign="center" includeIn="error">
 		<s:Label id="progressErrorLbl" styleName="progressErrorLblStyle" width="540"/>
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/PresentationWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/PresentationWindow.mxml
index ffe5dd7bb632b4afb386cdb6c15bdf3c6831cd5e..67617d8224335eeaad4b4b0e13a107b323eea41a 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/PresentationWindow.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/PresentationWindow.mxml
@@ -69,6 +69,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			
 			import mx.collections.ArrayCollection;
 			import mx.controls.Menu;
+			import mx.core.FlexGlobals;
 			import mx.events.MenuEvent;
 			import mx.managers.PopUpManager;
 			
@@ -298,7 +299,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			}
 						
 			private function onSliderZoom():void {
-				slideView.onZoomSlide(zoomSlider.value);
+				if (UsersUtil.amIPresenter()) {
+					slideView.onZoomSlide(zoomSlider.value);
+				}
 			}
 			
 			private function onResetZoom():void {
@@ -634,7 +637,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
             private function sendStartCustomPollEvent(pollType:String):void {
                 // Let's reset the page to display full size so we can display the result
                 // on the bottom right-corner.
-                var pollChoicesPopUp:PollChoicesModal = PopUpUtil.createModalPopUp(this, PollChoicesModal, true) as PollChoicesModal;
+                var pollChoicesPopUp:PollChoicesModal = PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, PollChoicesModal, true) as PollChoicesModal;
                 if (pollChoicesPopUp) {
                     pollChoicesPopUp.setPresentationWindow(this);
                 }
@@ -695,7 +698,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 				// the event for this doesn't exist yet
 				if (UsersUtil.amIPresenter()) {
 					// display the results view
-					var pollResultsPopUp : PollResultsModal = PopUpUtil.createModalPopUp(this, PollResultsModal, true) as PollResultsModal;
+					var pollResultsPopUp : PollResultsModal = PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, PollResultsModal, true) as PollResultsModal;
 					if (pollResultsPopUp) {
 						pollResultsPopUp.setPoll(e.poll);
 					}
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageSender.as b/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageSender.as
index 55191ff04ce17a9de8a0f001d598f4d5b2c76050..ca4de961b3082292063a24e82c17961f76d77195 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageSender.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageSender.as
@@ -328,7 +328,7 @@ package org.bigbluebutton.modules.users.services
       var message:Object = {
         header: {name: "MuteMeetingCmdMsg", meetingId: UsersUtil.getInternalMeetingID(), 
           userId: UsersUtil.getMyUserID()},
-        body: {mutedBy: UsersUtil.getMyUserID()}
+        body: {mutedBy: UsersUtil.getMyUserID(), mute: mute}
       };
       
       var _nc:ConnectionManager = BBB.initConnectionManager();
@@ -349,7 +349,7 @@ package org.bigbluebutton.modules.users.services
       var message:Object = {
         header: {name: "MuteAllExceptPresentersCmdMsg", meetingId: UsersUtil.getInternalMeetingID(), 
           userId: UsersUtil.getMyUserID()},
-        body: {mutedBy: UsersUtil.getMyUserID()}
+        body: {mutedBy: UsersUtil.getMyUserID(), mute: mute}
       };
 
       var _nc:ConnectionManager = BBB.initConnectionManager();
@@ -370,7 +370,7 @@ package org.bigbluebutton.modules.users.services
       var message:Object = {
         header: {name: "MuteUserCmdMsg", meetingId: UsersUtil.getInternalMeetingID(), 
           userId: UsersUtil.getMyUserID()},
-        body: {userId: userid, mutedBy: UsersUtil.getMyUserID()}
+        body: {userId: userid, mutedBy: UsersUtil.getMyUserID(), mute: mute}
       };
 
       var _nc:ConnectionManager = BBB.initConnectionManager();
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/UsersWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/UsersWindow.mxml
index 1992f62e615156a607c9cc234f34714f0e71cea9..8c5e847bc69bb14ac5b38d4e038d9c0e5b5f327f 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/UsersWindow.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/UsersWindow.mxml
@@ -469,7 +469,7 @@ $Id: $
       private function handleBreakoutJoinUrl(event:BreakoutRoomEvent):void {
         // We display only one alert
         removeJoinWindow();
-		joinWindow = PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, JoinBreakoutRoomWindow) as JoinBreakoutRoomWindow;
+		joinWindow = PopUpUtil.createModalPopUp(FlexGlobals.topLevelApplication as DisplayObject, JoinBreakoutRoomWindow, true) as JoinBreakoutRoomWindow;
 		joinWindow.setBreakoutRoomSettings(event.breakoutMeetingSequence, event.joinURL);
       }
       
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 c75fb84c962787a77fb637cbaefdc95f5144ba94..d46afbdfe8a5bcbd38538de9087d6e95c4939d9c 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/ToolbarPopupButton.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/ToolbarPopupButton.mxml
@@ -153,7 +153,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 {
@@ -190,6 +189,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-config/web/index_html5_vs_flash.html b/bigbluebutton-config/web/index_html5_vs_flash.html
index 0a7e2fb139de4d9a6888ecdfa806e6e7b0dd0328..fb92c0d8a9d21807db2734abf0e6ca83ce1f1cfd 100644
--- a/bigbluebutton-config/web/index_html5_vs_flash.html
+++ b/bigbluebutton-config/web/index_html5_vs_flash.html
@@ -85,12 +85,12 @@
        <div >
           <h2>BigBlueButton HTML5 client test server</h2>
           <p> <a href="http://bigbluebutton.org/" target="_blank">BigBlueButton</a> is an open source web conferencing system for on-line learning. This is a public test server for the BigBlueButton <a href="http://docs.bigbluebutton.org/html/html5-overview.html">HTML5 client</a> currently under development.</p>
-	<p> Our goal for the upcoming release of the HTML5 client is to implement all the <a href="https://youtu.be/oh0bEk3YSwI">viewer capabilities</a> of the Flash client.  Students join online classes as a viewer.  The HTML5 client will give remote students the ability to join from their Android mobile devices.  Users using the Flash and HTML5 clients can join the same meeting (hence the two choices above).  We built the HTML5 client using web real-time communication (WebRTC), <a href="https://facebook.github.io/react/">React</a>, and <a href="https://www.mongodb.com/">MongoDB</a>.</p>
-	<p>  The HTML5 works well with desktop and Android devices (phone and tablets) as they all support WebRTC.  Apple does not (yet) support WebRTC in Safari for iOS devices, but don't worry -- we are working in parallel on app for iOS devices.   What can this developer build of the HTML5 client do right now?  Pretty much everything the Flash client can do for viewers except (a) view a desktop sharing stream from the presenter and (b) send/receive webcam streams.  We're working on (a) and (b).  For now, we are really happy to share with you our progress and get <a href="https://docs.google.com/forms/d/1gFz5JdN3vD6jxhlVskFYgtEKEcexdDnUzpkwUXwQ4OY/viewform?usp=send_for">your feedback</a> on what has been implemeted so far.  Enjoy!</p>
+        <p> Our goal for the upcoming release of the HTML5 client is to implement all the <a href="https://youtu.be/oh0bEk3YSwI">viewer capabilities</a> of the Flash client.  Students join online classes as a viewer.  The HTML5 client will give remote students the ability to join from their Android and Apple (iOS 11+) devices.  Users using the Flash and HTML5 clients can join the same meeting (hence the two choices above).  We built the HTML5 client using web real-time communication (WebRTC), <a href="https://facebook.github.io/react/">React</a>, and <a href="https://www.mongodb.com/">MongoDB</a>.</p>
+        <p>  What can this developer build of the HTML5 client do right now?  Pretty much everything the Flash client can do for viewers except (a) view a desktop sharing stream from the presenter and (b) send/receive webcam streams.  We're working on (a) and (b).  For now, we are really happy to share with you our progress and get <a href="https://docs.google.com/forms/d/1gFz5JdN3vD6jxhlVskFYgtEKEcexdDnUzpkwUXwQ4OY/viewform?usp=send_for">your feedback</a> on what has been implemeted so far.  Enjoy!</p>
 
           <h4>For Developers</h4>
           <p> The BigBlueButton project is <a href="http://bigbluebutton.org/support">supported</a> by a community of developers that care about good design and a streamlined user experience. </p>
-          <p>See <a href="/demo/demo1.jsp" target="_blank">API examples </a> for how to integrate BigBlueButton with your project.</p>
+          <p>See <a href="http://docs.bigblubutton.org" target="_blank">Documentation</a> for more information on how you can integrate BigBlueButton with your project.</p>
         </div>
         <div class="span one"></div>
 
@@ -98,6 +98,7 @@
 
 
 
+
       <hr class="featurette-divider">
 
       <!-- BigBlueButton Features -->
diff --git a/bigbluebutton-html5/client/main.html b/bigbluebutton-html5/client/main.html
old mode 100755
new mode 100644
index 19441af93e6dc23e051acc57515bf13c9cea2fa8..e29b01b158425285740c30daf634096ab0ae608f
--- 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/polls/server/eventHandlers.js b/bigbluebutton-html5/imports/api/polls/server/eventHandlers.js
index 0f3062c9ffb56a9deb8599d4e7d5ea93f644ec14..91bf131a7535153f50f3930a285da21b87e0ce67 100644
--- a/bigbluebutton-html5/imports/api/polls/server/eventHandlers.js
+++ b/bigbluebutton-html5/imports/api/polls/server/eventHandlers.js
@@ -7,4 +7,4 @@ import handleUserVoted from './handlers/userVoted';
 RedisPubSub.on('PollShowResultEvtMsg', handlePollPublished);
 RedisPubSub.on('PollStartedEvtMsg', handlePollStarted);
 RedisPubSub.on('PollStoppedEvtMsg', handlePollStopped);
-RedisPubSub.on('UserRespondedToPollEvtMsg', handleUserVoted);
+RedisPubSub.on('PollUpdatedEvtMsg', handleUserVoted);
diff --git a/bigbluebutton-html5/imports/api/polls/server/handlers/userVoted.js b/bigbluebutton-html5/imports/api/polls/server/handlers/userVoted.js
index 5b80a46d28386a9921d3653bf78801775411fdf2..d73d8b67909854027850b16ecadcfe1829cb6105 100644
--- a/bigbluebutton-html5/imports/api/polls/server/handlers/userVoted.js
+++ b/bigbluebutton-html5/imports/api/polls/server/handlers/userVoted.js
@@ -3,11 +3,20 @@ import updateVotes from '../modifiers/updateVotes';
 
 export default function userVoted({ body }, meetingId) {
   const { poll } = body;
-  const { presenterId } = body;
 
   check(meetingId, String);
-  check(poll, Object);
-  check(presenterId, String);
+  check(poll, {
+    id: String,
+    answers: [
+      {
+        id: Number,
+        key: String,
+        numVotes: Number,
+      },
+    ],
+    numRespondents: Number,
+    numResponders: Number,
+  });
 
-  return updateVotes(poll, meetingId, presenterId);
+  return updateVotes(poll, meetingId);
 }
diff --git a/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js b/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js
index bc59d38e06d85cadeb75388256af9206beb725c2..011f21411ea431862f1bc721a256a38e9f9d5033 100644
--- a/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js
+++ b/bigbluebutton-html5/imports/api/polls/server/methods/publishVote.js
@@ -10,6 +10,11 @@ export default function publishVote(credentials, id, pollAnswerId) { // TODO dis
 
   const { meetingId, requesterUserId } = credentials;
 
+  /*
+   We keep an array of people who were in the meeting at the time the poll
+   was started. The poll is published to them only.
+   Once they vote - their ID is removed and they cannot see the poll anymore
+   */
   const currentPoll = Polls.findOne({
     users: requesterUserId,
     meetingId,
diff --git a/bigbluebutton-html5/imports/api/polls/server/modifiers/updateVotes.js b/bigbluebutton-html5/imports/api/polls/server/modifiers/updateVotes.js
index 3cb37bd1dab58047d9bb316397a9b1f089adaac0..30815aafa1c1d1d8fc90e15f5cc983dc5de3f40d 100644
--- a/bigbluebutton-html5/imports/api/polls/server/modifiers/updateVotes.js
+++ b/bigbluebutton-html5/imports/api/polls/server/modifiers/updateVotes.js
@@ -3,19 +3,17 @@ import { check } from 'meteor/check';
 import Logger from '/imports/startup/server/logger';
 import flat from 'flat';
 
-export default function updateVotes(poll, meetingId, requesterId) {
+export default function updateVotes(poll, meetingId) {
   check(meetingId, String);
-  check(requesterId, String);
   check(poll, Object);
 
   const {
     id,
     answers,
+    numResponders,
+    numRespondents,
   } = poll;
 
-  const { numResponders } = poll;
-  const { numRespondents } = poll;
-
   check(id, String);
   check(answers, Array);
 
@@ -24,15 +22,11 @@ export default function updateVotes(poll, meetingId, requesterId) {
 
   const selector = {
     meetingId,
-    requester: requesterId,
     id,
   };
 
   const modifier = {
-    $set: Object.assign(
-      { requester: requesterId },
-      flat(poll, { safe: true }),
-    ),
+    $set: flat(poll, { safe: true }),
   };
 
   const cb = (err) => {
diff --git a/bigbluebutton-html5/imports/api/presentations/server/eventHandlers.js b/bigbluebutton-html5/imports/api/presentations/server/eventHandlers.js
index 54a044bb2fc637761ba6a3e4f813f2cfb1704a06..620a0eac506f6d063eedc17db71dabffccbbd585 100644
--- a/bigbluebutton-html5/imports/api/presentations/server/eventHandlers.js
+++ b/bigbluebutton-html5/imports/api/presentations/server/eventHandlers.js
@@ -7,6 +7,7 @@ import handlePresentationConversionUpdate from './handlers/presentationConversio
 
 RedisPubSub.on('SyncGetPresentationInfoRespMsg', handlePresentationInfoReply);
 RedisPubSub.on('PresentationPageGeneratedEvtMsg', handlePresentationConversionUpdate);
+RedisPubSub.on('PresentationPageCountErrorEvtMsg', handlePresentationConversionUpdate);
 RedisPubSub.on('PresentationConversionUpdateEvtMsg', handlePresentationConversionUpdate);
 RedisPubSub.on('PresentationConversionCompletedEvtMsg', handlePresentationAdded);
 RedisPubSub.on('RemovePresentationEvtMsg', handlePresentationRemove);
diff --git a/bigbluebutton-html5/imports/api/presentations/server/handlers/presentationConversionUpdate.js b/bigbluebutton-html5/imports/api/presentations/server/handlers/presentationConversionUpdate.js
index a8c192261342d621083d34ba86fed75dd7b520c4..4b82690f75f589cccb1addafbe98831119012d40 100644
--- a/bigbluebutton-html5/imports/api/presentations/server/handlers/presentationConversionUpdate.js
+++ b/bigbluebutton-html5/imports/api/presentations/server/handlers/presentationConversionUpdate.js
@@ -49,6 +49,7 @@ export default function handlePresentationConversionUpdate({ body }, meetingId)
       statusModifier.id = presentationId;
       statusModifier.name = presentationName;
       statusModifier['conversion.error'] = true;
+      statusModifier['conversion.done'] = true;
       break;
 
     case GENERATED_SLIDE_KEY:
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/handlers/joinVoiceUser.js b/bigbluebutton-html5/imports/api/voice-users/server/handlers/joinVoiceUser.js
index 0b0d8d65b40aa61425519c4a4423ca55256e27f7..949df6d6cf29a61606bd150bf2deb1fd62bd2fff 100644
--- a/bigbluebutton-html5/imports/api/voice-users/server/handlers/joinVoiceUser.js
+++ b/bigbluebutton-html5/imports/api/voice-users/server/handlers/joinVoiceUser.js
@@ -1,5 +1,7 @@
 import { check } from 'meteor/check';
 
+import Logger from '/imports/startup/server/logger';
+import Users from '/imports/api/users';
 import addVoiceUser from '../modifiers/addVoiceUser';
 
 export default function handleJoinVoiceUser({ body }, meetingId) {
@@ -7,6 +9,74 @@ export default function handleJoinVoiceUser({ body }, meetingId) {
   voiceUser.joined = true;
 
   check(meetingId, String);
+  check(voiceUser, {
+    voiceConf: String,
+    intId: String,
+    voiceUserId: String,
+    callerName: String,
+    callerNum: String,
+    muted: Boolean,
+    talking: Boolean,
+    callingWith: String,
+    listenOnly: Boolean,
+    joined: Boolean,
+  });
+
+  const {
+    intId,
+    callerName,
+  } = voiceUser;
+
+  if (intId.toString().startsWith('v_')) {
+    /* voice-only user - called into the conference */
+
+    const selector = {
+      meetingId,
+      userId: intId,
+    };
+
+    const USER_CONFIG = Meteor.settings.public.user;
+    const ROLE_VIEWER = USER_CONFIG.role_viewer;
+
+    const modifier = {
+      $set: {
+        meetingId,
+        connectionStatus: 'online',
+        roles: [ROLE_VIEWER.toLowerCase()],
+        sortName: callerName.trim().toLowerCase(),
+        color: '#ffffff', // TODO
+        intId,
+        extId: intId, // TODO
+        name: callerName,
+        role: ROLE_VIEWER.toLowerCase(),
+        guest: false,
+        authed: true,
+        waitingForAcceptance: false,
+        emoji: 'none',
+        presenter: false,
+        locked: false, // TODO
+        avatar: '',
+      },
+    };
+
+    const cb = (err, numChanged) => {
+      if (err) {
+        return Logger.error(`Adding call-in user to VoiceUser collection: ${err}`);
+      }
+
+      const { insertedId } = numChanged;
+      if (insertedId) {
+        return Logger.info(`Added a call-in user id=${intId} meeting=${meetingId}`);
+      }
+
+      return Logger.info(`Upserted a call-in user id=${intId} meeting=${meetingId}`);
+    };
+
+    Users.upsert(selector, modifier, cb);
+  } else {
+
+    /* there is a corresponding web user in Users collection -- no need to add new one */
+  }
 
   return addVoiceUser(meetingId, voiceUser);
 }
diff --git a/bigbluebutton-html5/imports/api/voice-users/server/handlers/leftVoiceUser.js b/bigbluebutton-html5/imports/api/voice-users/server/handlers/leftVoiceUser.js
index e2e4baaa154249d57a35afad4bffa01ee4c9cde8..5fb2e788b3a950300cc9b731ad6bae99e35389f7 100644
--- a/bigbluebutton-html5/imports/api/voice-users/server/handlers/leftVoiceUser.js
+++ b/bigbluebutton-html5/imports/api/voice-users/server/handlers/leftVoiceUser.js
@@ -1,11 +1,20 @@
 import { check } from 'meteor/check';
 
-import removeVoiceUser from '../modifiers/removeVoiceUser';
+import removeVoiceUser from '/imports/api/voice-users/server/modifiers/removeVoiceUser';
+import removeUser from '/imports/api/users/server/modifiers/removeUser';
 
 export default function handleVoiceUpdate({ body }, meetingId) {
   const voiceUser = body;
 
   check(meetingId, String);
+  check(voiceUser, {
+    voiceConf: String,
+    intId: String,
+    voiceUserId: String,
+  });
 
+  const { intId } = voiceUser;
+
+  removeUser(meetingId, intId);
   return removeVoiceUser(meetingId, voiceUser);
 }
diff --git a/bigbluebutton-html5/imports/api/voice-users/server/methods.js b/bigbluebutton-html5/imports/api/voice-users/server/methods.js
index 8b1515111a87a191036adc41126c8a5cf2e8e99b..33e995e1146a1e31c7ea248897fc76befc096792 100644
--- a/bigbluebutton-html5/imports/api/voice-users/server/methods.js
+++ b/bigbluebutton-html5/imports/api/voice-users/server/methods.js
@@ -2,10 +2,12 @@ import { Meteor } from 'meteor/meteor';
 import mapToAcl from '/imports/startup/mapToAcl';
 import listenOnlyToggle from './methods/listenOnlyToggle';
 import muteToggle from './methods/muteToggle';
+import ejectUserFromVoice from './methods/ejectUserFromVoice';
 
-Meteor.methods(mapToAcl(['methods.listenOnlyToggle', 'methods.toggleSelfVoice', 'methods.toggleVoice',
-], {
+Meteor.methods(mapToAcl(['methods.listenOnlyToggle', 'methods.toggleSelfVoice',
+  'methods.toggleVoice', 'methods.ejectUserFromVoice'], {
   listenOnlyToggle,
   toggleSelfVoice: (credentials) => { muteToggle(credentials, credentials.requesterUserId); },
   toggleVoice: muteToggle,
+  ejectUserFromVoice,
 }));
diff --git a/bigbluebutton-html5/imports/api/voice-users/server/methods/ejectUserFromVoice.js b/bigbluebutton-html5/imports/api/voice-users/server/methods/ejectUserFromVoice.js
new file mode 100644
index 0000000000000000000000000000000000000000..73c61eb95baa8bb86a72a3318873895c2c284d3d
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/voice-users/server/methods/ejectUserFromVoice.js
@@ -0,0 +1,22 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import RedisPubSub from '/imports/startup/server/redis';
+
+export default function ejectUserFromVoice(credentials, userId) {
+  const REDIS_CONFIG = Meteor.settings.redis;
+  const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
+  const EVENT_NAME = 'EjectUserFromVoiceCmdMsg';
+
+  const { requesterUserId, meetingId } = credentials;
+
+  check(meetingId, String);
+  check(requesterUserId, String);
+  check(userId, String);
+
+  const payload = {
+    userId,
+    ejectedBy: requesterUserId,
+  };
+
+  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 1234d484365ce250ffca5484e3794e034623f450..ea1d4dedd33eacfab09d102f91d291917435a43a 100644
--- a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js
+++ b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js
@@ -1,21 +1,51 @@
 import { Meteor } from 'meteor/meteor';
 import { check } from 'meteor/check';
 import RedisPubSub from '/imports/startup/server/redis';
+import Users from '/imports/api/users';
+import VoiceUsers from '/imports/api/voice-users';
 
 export default function muteToggle(credentials, userId) {
   const REDIS_CONFIG = Meteor.settings.redis;
   const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
   const EVENT_NAME = 'MuteUserCmdMsg';
+  const APP_CONFIG = Meteor.settings.public.app;
+  const ALLOW_MODERATOR_TO_UNMUTE_AUDIO = APP_CONFIG.allowModeratorToUnmuteAudio;
+  const USER_CONFIG = Meteor.settings.public.user;
+  const ROLE_MODERATOR = USER_CONFIG.role_moderator;
 
   const { meetingId, requesterUserId } = credentials;
 
   check(meetingId, String);
   check(requesterUserId, String);
 
+  const requester = Users.findOne({
+    meetingId,
+    userId: requesterUserId,
+  });
+
+  const voiceUser = VoiceUsers.findOne({
+    intId: userId,
+  });
+
+  if (!requester || !voiceUser) return;
+
+  const { listenOnly, muted } = voiceUser;
+  if (listenOnly) return;
+
+  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 &&
+    muted &&
+    isNotHimself) return;
+
   const payload = {
     userId,
     mutedBy: requesterUserId,
+    mute: !muted,
   };
 
-  return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, userId, payload);
+  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 cccf78cc5b2a9bb1c4d01f749f927ebd839b0593..8e15d2021a9bcffbfcc3d123d977f9b184c190a9 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,
@@ -79,11 +80,14 @@ 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
+            hideLabel
+            aria-label={intl.formatMessage(intlMessages.actionsLabel)}
+            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 0ab79ef349c49eaa2d7cd8522d7cfc3712d66eeb..f671845580c4b104a657ac7703250d265a14d81b 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
@@ -1,6 +1,6 @@
 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';
 import JoinVideoOptionsContainer from '../video-dock/video-menu/container';
@@ -12,6 +12,9 @@ const ActionsBar = ({
   handleShareScreen,
   handleUnshareScreen,
   isVideoBroadcasting,
+  emojiList,
+  emojiSelected,
+  handleEmojiChange,
 }) => (
   <div className={styles.actionsbar}>
     <div className={styles.left}>
@@ -25,7 +28,7 @@ const ActionsBar = ({
           handleCloseVideo={handleExitVideo}
         />
       : null}
-      <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 bcc1935453241983d209ef7f99bdbcf9bcea50c9..930bb2afe9fd9946fa01a5d871f75fc4fbcaf206 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
@@ -1,40 +1,21 @@
 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 VideoService from '../video-dock/service';
 import ScreenshareService from '../screenshare/service';
 
-import AudioModal from '../audio/audio-modal/component';
+const ActionsBarContainer = props => <ActionsBar {...props} />;
 
-const ActionsBarContainer = ({ children, ...props }) => (
-  <ActionsBar {...props}>
-    {children}
-  </ActionsBar>
-);
+export default createContainer(() => ({
+  isUserPresenter: Service.isUserPresenter(),
+  emojiList: Service.getEmojiList(),
+  emojiSelected: Service.getEmoji(),
+  handleEmojiChange: Service.setEmoji,
+  handleExitVideo: VideoService.exitVideo();
+  handleJoinVideo: VideoService.joinVideo();
+  handleShareScreen: ScreenshareService.shareScreen();
+  handleUnshareScreen: ScreenshareService.unshareScreen();
+  isVideoBroadcasting: ScreenshareService.isVideoBroadcasting();
 
-export default withModalMounter(createContainer(({ mountModal }) => {
-  const isPresenter = Service.isUserPresenter();
-
-  const handleExitAudio = () => AudioService.exitAudio();
-  const handleOpenJoinAudio = () =>
-    mountModal(<AudioModal handleJoinListenOnly={AudioService.joinListenOnly} />);
-  const handleExitVideo = () => VideoService.exitVideo();
-  const handleJoinVideo = () => VideoService.joinVideo();
-  const handleShareScreen = () => ScreenshareService.shareScreen();
-  const handleUnshareScreen = () => ScreenshareService.unshareScreen();
-  const isVideoBroadcasting = () => ScreenshareService.isVideoBroadcasting();
-
-  return {
-    isUserPresenter: isPresenter,
-    handleExitAudio,
-    handleOpenJoinAudio,
-    handleExitVideo,
-    handleJoinVideo,
-    handleShareScreen,
-    handleUnshareScreen,
-    isVideoBroadcasting
-  };
-}, ActionsBarContainer));
+}), 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..2ba85150eb402e0e533759ed2632e0b5c35e34cc
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-select/component.jsx
@@ -0,0 +1,103 @@
+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();
+
+  const statusLabel = statuses.indexOf(selected) === -1 ?
+    intl.formatMessage(intlMessages.statusTriggerLabel)
+    : intl.formatMessage({ id: `app.actionsBar.emojiMenu.${selected}Label` });
+
+  return (
+    <Dropdown autoFocus>
+      <DropdownTrigger tabIndex={0}>
+        <Button
+          className={styles.button}
+          label={statusLabel}
+          aria-label={statusLabel}
+          aria-describedby="currentStatus"
+          icon={options[selected !== lastStatus ? selected : statuses[1]]}
+          ghost={false}
+          hideLabel
+          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..f03be42938e3da9c42dfd6fdacacebb6a55e048a 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx
@@ -1,7 +1,29 @@
 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 styles from './styles';
+import cx from 'classnames';
+
+
+const intlMessages = defineMessages({
+  joinAudio: {
+    id: 'app.audio.joinAudio',
+    description: 'Join audio button label',
+  },
+  leaveAudio: {
+    id: 'app.audio.leaveAudio',
+    description: 'Leave audio button label',
+  },
+  muteAudio: {
+    id: 'app.actionsBar.muteLabel',
+    description: 'Mute audio button label',
+  },
+  unmuteAudio: {
+    id: 'app.actionsBar.unmuteLabel',
+    description: 'Unmute audio button label',
+  },
+});
 
 const propTypes = {
   handleToggleMuteMicrophone: PropTypes.func.isRequired,
@@ -11,6 +33,7 @@ const propTypes = {
   unmute: PropTypes.bool.isRequired,
   mute: PropTypes.bool.isRequired,
   join: PropTypes.bool.isRequired,
+  intl: intlShape.isRequired,
 };
 
 const AudioControls = ({
@@ -20,15 +43,19 @@ const AudioControls = ({
   mute,
   unmute,
   disable,
+  glow,
   join,
+  intl,
 }) => (
   <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'}
+        hideLabel
+        label={unmute ? intl.formatMessage(intlMessages.unmuteAudio) : intl.formatMessage(intlMessages.muteAudio)}
+        aria-label={unmute ? intl.formatMessage(intlMessages.unmuteAudio) : intl.formatMessage(intlMessages.muteAudio)}
         color={'primary'}
         icon={unmute ? 'mute' : 'unmute'}
         size={'lg'}
@@ -38,7 +65,9 @@ const AudioControls = ({
       className={styles.button}
       onClick={join ? handleLeaveAudio : handleJoinAudio}
       disabled={disable}
-      label={join ? 'Leave Audio' : 'Join Audio'}
+      hideLabel
+      aria-label={join ? intl.formatMessage(intlMessages.leaveAudio) : intl.formatMessage(intlMessages.joinAudio)}
+      label={join ? intl.formatMessage(intlMessages.leaveAudio) : intl.formatMessage(intlMessages.joinAudio)}
       color={join ? 'danger' : 'primary'}
       icon={join ? 'audio_off' : 'audio_on'}
       size={'lg'}
@@ -48,4 +77,4 @@ const AudioControls = ({
 
 AudioControls.propTypes = propTypes;
 
-export default AudioControls;
+export default injectIntl(AudioControls);
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 0d3e497c58bbc4476cad217521cab43248737039..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;
 }
@@ -36,12 +42,8 @@
 }
 
 .closeBtn {
-  right: 0;
-  top: 0;
-  position: absolute;
+  position: relative;
   background-color: $color-white;
-  border: none;
-  padding: .75rem;
 
   i {
     color: $color-gray-light;
@@ -49,9 +51,9 @@
 
   &:focus,
   &:hover{
-    background-color: $color-white;
+    background-color: $color-gray-lighter;
     i{
-      color: $color-primary;
+      color: $color-gray;
     }
   }
 }
@@ -106,6 +108,8 @@
 }
 
 .connecting {
+  margin-top: auto;
+  margin-bottom: auto;
   font-size: 2rem;
 }
 
@@ -138,4 +142,4 @@
   color: $color-text;
   display: inline-block;
   font-size: 0.9rem;
-}
+}
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/component.jsx b/bigbluebutton-html5/imports/ui/components/button/component.jsx
index d15656fedd7fc503618946249006d47f3d71ea82..c8618ba5f0abb2f51696197ca29f578c6c34c605 100644
--- a/bigbluebutton-html5/imports/ui/components/button/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/button/component.jsx
@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import cx from 'classnames';
+import Tooltip from '/imports/ui/components/tooltip/component';
 import styles from './styles';
 import Icon from '../icon/component';
 import BaseButton from './base/component';
@@ -86,7 +87,6 @@ const defaultProps = {
 };
 
 export default class Button extends BaseButton {
-
   _getClassNames() {
     const {
       size,
@@ -113,8 +113,26 @@ export default class Button extends BaseButton {
   }
 
   render() {
-    const renderFuncName = this.props.circle ?
-      'renderCircle' : 'renderDefault';
+    const {
+      circle,
+      hideLabel,
+      label,
+      'aria-label' : ariaLabel
+    } = this.props;
+
+    const renderFuncName = circle ? 'renderCircle' : 'renderDefault';
+
+    if (hideLabel) {
+      const tooltipLabel = label ? label : ariaLabel;
+
+      return (
+        <Tooltip
+          title={tooltipLabel}
+        >
+          {this[renderFuncName]()}
+        </Tooltip>
+      );
+    }
 
     return this[renderFuncName]();
   }
diff --git a/bigbluebutton-html5/imports/ui/components/button/styles.scss b/bigbluebutton-html5/imports/ui/components/button/styles.scss
index 7124c89d82e30f1a0d537b2695891098a3760ad5..89a0a15a201aec54775690b5dff355e1ae9222ec 100644
--- a/bigbluebutton-html5/imports/ui/components/button/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/button/styles.scss
@@ -37,10 +37,13 @@ $btn-jumbo-padding: $jumbo-padding-y $jumbo-padding-x;
  * ==========
  */
 .button {
-  @extend %btnFocus;
-
+  border: none;
+  outline: none;
+  overflow: visible;
+  display: inline-block;
+  border-radius: $border-size;
   font-weight: $btn-font-weight;
-  line-height: 1.5;
+  line-height: 1;
   text-align: center;
   white-space: nowrap;
   vertical-align: middle;
@@ -69,14 +72,17 @@ $btn-jumbo-padding: $jumbo-padding-y $jumbo-padding-x;
   &:active {
     &:focus {
       span:first-of-type::before {
-        border-radius: $focus-border-size;
+        border-radius: $border-size;
       }
     }
   }
 }
 
 .buttonWrapper {
-  @extend %btnFocus;
+  border: none;
+  overflow: visible !important;
+  display: inline-block;
+  outline: none;
 
   &,
   &:active {
@@ -94,14 +100,6 @@ $btn-jumbo-padding: $jumbo-padding-y $jumbo-padding-x;
   background: none;
   padding: 0 !important;
 
-  &:active {
-    &:focus {
-      outline: thin dotted;
-      outline: 5px auto -webkit-focus-ring-color;
-      outline-offset: -2px;
-    }
-  }
-
   &[aria-disabled="true"] {
     cursor: not-allowed;
     opacity: .65;
@@ -135,6 +133,9 @@ $btn-jumbo-padding: $jumbo-padding-y $jumbo-padding-x;
   & + .button  {
     margin-left: $btn-spacing;
   }
+  &:hover {
+    opacity: .5;
+  }
 }
 
 .hideLabel {
@@ -159,6 +160,9 @@ $btn-jumbo-padding: $jumbo-padding-y $jumbo-padding-x;
   & + .label {
     margin-left: $btn-spacing;
   }
+  .buttonWrapper:hover & {
+    opacity: .75;
+  }
 }
 
 /* Colors
@@ -171,30 +175,24 @@ $btn-jumbo-padding: $jumbo-padding-y $jumbo-padding-x;
 
   color: $color;
   background-color: $background;
-  border-color: $border;
+  border: $border-size-large solid transparent;
 
-  &:hover,
   &:focus,
-  .buttonWrapper:hover &,
   .buttonWrapper:focus & {
+    outline: none !important;
     color: $color;
-    background-color: $active-background;
-    border-color: $active-border;
+    background-color: $background;
+    background-clip: padding-box;
+    border: $border-size-large solid transparent;
+    box-shadow: 0 0 0 $border-size $border;
   }
 
-  &:active,
-  .buttonWrapper:active & {
+  &:hover,
+  .buttonWrapper:hover & {
     color: $color;
     background-color: $active-background;
     border-color: $active-border;
-    background-image: none;
-
-    &:hover,
-    &:focus {
-      color: $color;
-      background-color: darken($background, 17%);
-      border-color: darken($border, 25%);
-    }
+    border: $border-size-large solid transparent;
   }
 }
 
@@ -202,34 +200,29 @@ $btn-jumbo-padding: $jumbo-padding-y $jumbo-padding-x;
   color: $color;
   background-image: none;
   background-color: transparent;
-  border-color: $color;
+  border: $border-size-large solid transparent;
 
   &:focus,
-  &:hover,
-  .buttonWrapper:hover &,
   .buttonWrapper:focus & {
-    color: $variant;
-    background-color: $color;
-    border-color: $color;
+    color: $color;
+    outline: none !important;
+    background-color: $variant;
+    background-clip: padding-box;
+    border: $border-size-large solid transparent;
+    box-shadow: 0 0 0 $border-size $color;
   }
 
-  &:active,
-  .buttonWrapper:active & {
-    color: $variant;
-    background-color: $color;
-    border-color: $color;
-
-    &:hover,
-    &:focus {
-      color: $variant;
-      background-color: darken($color, 17%);
-      border-color: darken($color, 25%);
-    }
+  &:hover,
+  .buttonWrapper:hover & {
+    color: $color;
+    background-color: $variant;
+    border-color: $variant;
+    border: $border-size-large solid transparent;
   }
 }
 
 .default {
-  @include button-variant($btn-default-color, $btn-default-bg, $btn-default-border);
+  @include button-variant($btn-default-color, $btn-default-bg, $btn-primary-border);
 }
 
 .primary {
diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx
index cdc45ab0cb44d14b12616b8edf36f5775ffe1e47..c63a4f1958430b71208d1e967174168dfb0868af 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx
@@ -3,7 +3,6 @@ import { defineMessages, injectIntl } from 'react-intl';
 import { withModalMounter } from '/imports/ui/components/modal/service';
 import Clipboard from 'clipboard';
 import _ from 'lodash';
-import Icon from '/imports/ui/components/icon/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';
@@ -11,6 +10,8 @@ import DropdownList from '/imports/ui/components/dropdown/list/component';
 import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
 import Auth from '/imports/ui/services/auth';
 import Acl from '/imports/startup/acl';
+import Button from '/imports/ui/components/button/component';
+
 import ChatService from './../service';
 import styles from './styles';
 
@@ -112,17 +113,22 @@ class ChatDropdown extends Component {
     const availableActions = this.getAvailableActions();
 
     return (
+
       <Dropdown
         isOpen={this.state.isSettingOpen}
         onShow={this.onActionsShow}
         onHide={this.onActionsHide}
       >
-        <DropdownTrigger
-          tabIndex={0}
-          aria-label={intl.formatMessage(intlMessages.options)}
-          className={styles.btn}
-        >
-          <Icon iconName="more" />
+        <DropdownTrigger tabIndex={0}>
+          <Button
+            className={styles.btn}
+            icon="more"
+            ghost
+            circle
+            hideLabel
+            color="primary"
+            aria-label={intl.formatMessage(intlMessages.options)}
+          />
         </DropdownTrigger>
         <DropdownContent placement="bottom right">
           <DropdownList>
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 d45076abba5c8f1576b77289aaf34479039fb0a6..a2cf678fe342b76f42676786c2057b651beb0c34 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/styles.scss
@@ -1,6 +1,31 @@
 @import "/imports/ui/stylesheets/variables/_all";
 
+$icon-offset: -.4em;
+$square-side-length: 1.56rem;
+
 .btn {
   flex: 0 0;
   margin-top: auto;
+
+  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:first-child {
+      background-color: transparent !important;
+    }
+  }
+
 }
diff --git a/bigbluebutton-html5/imports/ui/components/chat/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/component.jsx
index 75ab3deeddd6cfdea4004c67878d1e4354331a12..d5fc22ae2b686196c6cf662bdd32dba3244f9a29 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/component.jsx
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import { Link } from 'react-router';
 import { defineMessages, injectIntl } from 'react-intl';
 import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
+import Button from '/imports/ui/components/button/component';
 import styles from './styles';
 import MessageForm from './message-form/component';
 import MessageList from './message-list/component';
@@ -56,10 +57,16 @@ const Chat = (props) => {
             <Link
               to="/users"
               role="button"
-              className={styles.closeIcon}
-              aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}
+              tabIndex={-1}
             >
-              <Icon iconName="close" onClick={() => actions.handleClosePrivateChat(chatID)} />
+              <Button
+                className={styles.closeBtn}
+                icon="close"
+                size="md"
+                hideLabel
+                onClick={() => actions.handleClosePrivateChat(chatID)}
+                aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}
+              />
             </Link> :
             <ChatDropdown />
         }
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 878bf6128d4714c8acd284546e86004266aa07d8..caebbfd4d10ae68f6a78755598643c56084422c8 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/message-form/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/chat/message-form/styles.scss
@@ -1,4 +1,5 @@
-@import "../../../stylesheets/variables/_all";
+@import "/imports/ui/stylesheets/mixins/focus";
+@import "/imports/ui/stylesheets/variables/_all";
 
 .form {
   flex-grow: 0;
@@ -42,7 +43,8 @@
 }
 
 .input {
-  @extend %customInputFocus;
+  @include inputFocus($color-blue-light);
+
   flex: 1;
   background: #fff;
   background-clip: padding-box;
@@ -70,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/message-list/styles.scss b/bigbluebutton-html5/imports/ui/components/chat/message-list/styles.scss
index 8e4af86678558bb1a5366391693313618bb818c6..6e26849390b6b74432fd030619f6fbbbd9190556 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/message-list/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/styles.scss
@@ -27,7 +27,11 @@ $padding: $md-padding-x;
   margin-right: -$padding;
   padding-right: $padding;
   padding-top: 0;
-  padding-bottom: $padding;
+  &:after {
+    content: "";
+    display: block;
+    height: $padding;
+  }
 }
 
 .unreadButton {
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
new file mode 100644
index 0000000000000000000000000000000000000000..128997988c4030623c500da7b98396597a175846
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/chat/notification/audio-notification/component.jsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import _ from 'lodash';
+import PropTypes from 'prop-types';
+
+const propTypes = {
+  play: PropTypes.bool.isRequired,
+  count: PropTypes.number.isRequired,
+};
+
+class ChatAudioNotification extends React.Component {
+  constructor(props) {
+    super(props);
+    this.audio = new Audio('/html5client/resources/sounds/notify.mp3');
+
+    this.handleAudioLoaded = this.handleAudioLoaded.bind(this);
+    this.playAudio = this.playAudio.bind(this);
+    this.componentDidUpdate = _.debounce(this.playAudio, 2000);
+  }
+
+  componentDidMount() {
+    this.audio.addEventListener('loadedmetadata', this.handleAudioLoaded);
+  }
+
+  shouldComponentUpdate(nextProps) {
+    return nextProps.count > this.props.count;
+  }
+
+  componentWillUnmount() {
+    this.audio.removeEventListener('loadedmetadata', this.handleAudioLoaded);
+  }
+
+  handleAudioLoaded() {
+    this.componentDidUpdate = _.debounce(this.playAudio, this.audio.duration * 1000);
+  }
+
+  playAudio() {
+    if (!this.props.play) return;
+
+    this.audio.play();
+  }
+
+  render() {
+    return null;
+  }
+}
+ChatAudioNotification.propTypes = propTypes;
+
+export default ChatAudioNotification;
diff --git a/bigbluebutton-html5/imports/ui/components/chat/notification/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/notification/component.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..8dbe2179f1b8ae6feba0366d84eba9482f62ae18
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/chat/notification/component.jsx
@@ -0,0 +1,93 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import ChatAudioNotification from './audio-notification/component';
+import ChatPushNotification from './push-notification/component';
+
+const propTypes = {
+  disableNotify: PropTypes.bool.isRequired,
+  openChats: PropTypes.arrayOf(PropTypes.object).isRequired,
+  disableAudio: PropTypes.bool.isRequired,
+};
+
+class ChatNotification extends Component {
+  constructor(props) {
+    super(props);
+    this.state = { notified: {} };
+  }
+
+  componentWillReceiveProps(nextProps) {
+    const {
+      openChats,
+      disableNotify,
+    } = this.props;
+
+    if (nextProps.disableNotify === false && disableNotify === true) {
+      const loadMessages = {};
+      openChats
+        .forEach((c) => {
+          loadMessages[c.id] = c.unreadCounter;
+        });
+      this.setState({ notified: loadMessages });
+      return;
+    }
+
+    const notifiedToClear = {};
+    openChats
+      .filter(c => c.unreadCounter === 0)
+      .forEach((c) => {
+        notifiedToClear[c.id] = 0;
+      });
+
+    this.setState(({ notified }) => ({
+      notified: {
+        ...notified,
+        ...notifiedToClear,
+      },
+    }));
+  }
+
+  render() {
+    const {
+      disableNotify,
+      disableAudio,
+      openChats,
+    } = this.props;
+
+    const unreadMessagesCount = openChats
+      .map(chat => chat.unreadCounter)
+      .reduce((a, b) => a + b, 0);
+
+    const shouldPlayAudio = !disableAudio && unreadMessagesCount > 0;
+
+    const chatsNotify = openChats
+      .filter(({ id, unreadCounter }) =>
+        unreadCounter > 0 &&
+        unreadCounter !== this.state.notified[id] &&
+        !disableNotify);
+
+    return (
+      <span>
+        <ChatAudioNotification play={shouldPlayAudio} count={unreadMessagesCount} />
+        {
+          chatsNotify.map(({ id, name, unreadCounter }) =>
+            (<ChatPushNotification
+              key={id}
+              name={name}
+              count={unreadCounter}
+              onOpen={() => {
+                this.setState(({ notified }) => ({
+                  notified: {
+                    ...notified,
+                    [id]: unreadCounter,
+                  },
+                }));
+              }}
+            />))
+        }
+      </span>
+    );
+  }
+}
+ChatNotification.propTypes = propTypes;
+
+export default ChatNotification;
diff --git a/bigbluebutton-html5/imports/ui/components/chat/notification/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/notification/container.jsx
index b58274514ba8d3331b25dbf99bbf445e08033e3c..ade6871a4c75c8d840c759296888dd34b61b65b2 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/notification/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/notification/container.jsx
@@ -1,46 +1,20 @@
-import React, { Component } from 'react';
-// import PropTypes from 'prop-types';
+import React from 'react';
 import { createContainer } from 'meteor/react-meteor-data';
-import _ from 'lodash';
-
-import Auth from '/imports/ui/services/auth';
 import UserListService from '/imports/ui/components/user-list/service';
 import Settings from '/imports/ui/services/settings';
+import ChatNotification from './component';
 
-class ChatNotificationContainer extends Component {
-  constructor(props) {
-    super(props);
-
-    this.audio = new Audio('/html5client/resources/sounds/notify.mp3');
-  }
-
-  componentDidUpdate(prevProps) {
-    if (this.props.unreadMessagesCount < prevProps.unreadMessagesCount) return;
-
-    this.playAudio();
-  }
-
-  playAudio() {
-    if (this.props.disableAudio) return;
-
-    _.debounce(() => this.audio.play(), this.audio.duration * 1000)();
-  }
-
-  render() {
-    return null;
-  }
-}
+const ChatNotificationContainer = props => (
+  <ChatNotification {...props} />
+);
 
 export default createContainer(() => {
   const AppSettings = Settings.application;
-
-  const unreadMessagesCount = UserListService.getOpenChats()
-    .map(chat => chat.unreadCounter)
-    .filter(userID => userID !== Auth.userID)
-    .reduce((a, b) => a + b, 0);
+  const openChats = UserListService.getOpenChats();
 
   return {
     disableAudio: !AppSettings.chatAudioNotifications,
-    unreadMessagesCount,
+    disableNotify: !AppSettings.chatPushNotifications,
+    openChats,
   };
 }, ChatNotificationContainer);
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
new file mode 100644
index 0000000000000000000000000000000000000000..b74528f4a78e766dbc7b571a90688b5f2c03949e
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/chat/notification/push-notification/component.jsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import _ from 'lodash';
+import injectNotify from '/imports/ui/components/toast/inject-notify/component';
+import { defineMessages, injectIntl, intlShape } from 'react-intl';
+
+const propTypes = {
+  intl: intlShape.isRequired,
+  count: PropTypes.number.isRequired,
+  name: PropTypes.string.isRequired,
+  notify: PropTypes.func.isRequired,
+  onOpen: PropTypes.func.isRequired,
+};
+
+const intlMessages = defineMessages({
+  appToastChatSigular: {
+    id: 'app.toast.chat.singular',
+    description: 'when entry a message',
+  },
+  appToastChatPlural: {
+    id: 'app.toast.chat.plural',
+    description: 'when entry various message',
+  },
+});
+
+class ChatPushNotification extends React.Component {
+  constructor(props) {
+    super(props);
+    this.showNotify = _.debounce(this.showNotify.bind(this), 1000);
+
+    this.componentDidMount = this.showNotify;
+    this.componentDidUpdate = this.showNotify;
+  }
+
+  showNotify() {
+    const {
+      intl,
+      count,
+      name,
+      notify,
+      onOpen,
+    } = this.props;
+
+    const message = intl.formatMessage(count > 1 ?
+      intlMessages.appToastChatPlural :
+      intlMessages.appToastChatSigular, {
+      0: count,
+      1: name,
+    });
+
+    return notify(message, 'info', 'chat', { onOpen });
+  }
+
+  render() {
+    return null;
+  }
+}
+ChatPushNotification.propTypes = propTypes;
+
+export default injectIntl(injectNotify(ChatPushNotification));
diff --git a/bigbluebutton-html5/imports/ui/components/chat/styles.scss b/bigbluebutton-html5/imports/ui/components/chat/styles.scss
index 1b1c84e9282e0787458a75bbf9f579f73e5f619e..fc45c9915d073631aae5e273506aa5c945f94c06 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/chat/styles.scss
@@ -1,4 +1,7 @@
-@import "../../stylesheets/variables/_all";
+@import "/imports/ui/stylesheets/mixins/focus";
+@import "/imports/ui/stylesheets/variables/_all";
+
+$icon-offset: -.4em;
 
 .chat {
   background-color: #fff;
@@ -11,14 +14,15 @@
 }
 
 .header {
-  margin-bottom: $md-padding-x;
   display: flex;
   flex-direction: row;
   align-items: left;
   flex-shrink: 0;
 
   a {
-    @extend %customLinkFocus;
+    @include elementFocus($color-primary);
+    padding-bottom: $sm-padding-y;
+    padding-left: $sm-padding-y;
     text-decoration: none;
     display: block;
   }
@@ -34,7 +38,22 @@
   flex: 1;
 }
 
-.closeIcon {
+.closeBtn {
+  background-color: $color-white;
   flex: 0 0;
-  margin-left: $sm-padding-x / 2;
+  padding: 0 0.25rem !important;
+
+  i {
+    font-size: 0.85em;
+    color: $color-gray-dark !important;
+    top: $icon-offset;
+  }
+
+  &:focus,
+  &:hover{
+    background-color: $color-white !important;
+    i{
+      color: $color-gray;
+    }
+  }
 }
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 5fbeb7020133224eb9e1cf72dd4e4c52eb567057..17a7005996f69bf55b13c85011a7871b26783b00 100644
--- a/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss
@@ -1,4 +1,7 @@
-@import "../../../stylesheets/variables/_all";
+@import "/imports/ui/stylesheets/variables/_all";
+
+$item-bg-focus: $color-blue-lightest;
+$item-border-focus: $color-blue-lighter;
 
 .list {
   list-style: none;
@@ -28,39 +31,34 @@
   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;
 }
 
 .item {
-  @extend %customListItemFocus;
+  display: flex;
   flex: 1 1 100%;
   padding: ($line-height-computed / 3) 0;
-  display: flex;
-  align-items: center;
-  justify-content: flex-start;
+  padding-left: ($line-height-computed / 3);
+
+  &:focus {
+    outline: none;
+    border-radius: $border-size;
+    box-shadow: 0 0 0 $border-size $item-border-focus;
+    background-color: $item-bg-focus;
+  }
 
   @include mq($small-only) {
     padding: ($line-height-computed / 1.5) 0;
     justify-content: center;
   }
 
-  &:first-child {
-    padding-top: 0;
-  }
-
-  &:last-child {
-    padding-bottom: 0;
-  }
-
   &:hover,
   &:focus {
     cursor: pointer;
-    color: $color-primary;
 
     .itemIcon,
     .itemLabel {
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/presentation/presentation-toolbar/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx
index a5412544b0461e8d1b4001653b31d5a4ed6b810b..95a5321fef63dd41052b1e0e6434848c2f920e52 100644
--- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx
@@ -14,6 +14,10 @@ const intlMessages = defineMessages({
     id: 'app.presentation.presentationToolbar.nextSlideLabel',
     description: 'Next slide button label',
   },
+  goToSlide: {
+    id: 'app.presentation.presentationToolbar.goToSlide',
+    description: 'button for slide select',
+  },
 });
 
 class PresentationToolbar extends Component {
@@ -114,22 +118,6 @@ class PresentationToolbar extends Component {
     );
   }
 
-  static renderSkipSlideOpts(numberOfSlides) {
-    // Fill drop down menu with all the slides in presentation
-    const optionList = [];
-    for (let i = 1; i <= numberOfSlides; i += 1) {
-      optionList.push(
-        <option
-          value={i}
-          key={i}
-        >
-        Slide {i}
-        </option>,
-      );
-    }
-
-    return optionList;
-  }
 
   constructor(props) {
     super(props);
@@ -153,6 +141,24 @@ class PresentationToolbar extends Component {
       fitToScreenValue: 'not_implemented_yet',
     });
   }
+  renderSkipSlideOpts(numberOfSlides) {
+    // Fill drop down menu with all the slides in presentation
+    const { intl } = this.props;
+    const optionList = [];
+    for (let i = 1; i <= numberOfSlides; i += 1) {
+      optionList.push((
+        <option
+          value={i}
+          key={i}
+        >
+          {
+            intl.formatMessage(intlMessages.goToSlide, { 0: i })
+          }
+        </option>));
+    }
+
+    return optionList;
+  }
 
   render() {
     const {
@@ -170,9 +176,9 @@ class PresentationToolbar extends Component {
           aria-labelledby="prevSlideLabel"
           aria-describedby="prevSlideDesc"
           disabled={!(currentSlideNum > 1)}
-          color={'default'}
-          icon={'left_arrow'}
-          size={'md'}
+          color="default"
+          icon="left_arrow"
+          size="md"
           onClick={actions.previousSlideHandler}
           label={intl.formatMessage(intlMessages.previousSlideLabel)}
           hideLabel
@@ -189,16 +195,16 @@ class PresentationToolbar extends Component {
           onChange={actions.skipToSlideHandler}
           className={styles.skipSlide}
         >
-          {PresentationToolbar.renderSkipSlideOpts(numberOfSlides)}
+          {this.renderSkipSlideOpts(numberOfSlides)}
         </select>
         <Button
           role="button"
           aria-labelledby="nextSlideLabel"
           aria-describedby="nextSlideDesc"
           disabled={!(currentSlideNum < numberOfSlides)}
-          color={'default'}
-          icon={'right_arrow'}
-          size={'md'}
+          color="default"
+          icon="right_arrow"
+          size="md"
           onClick={actions.nextSlideHandler}
           label={intl.formatMessage(intlMessages.nextSlideLabel)}
           hideLabel
@@ -256,7 +262,6 @@ class PresentationToolbar extends Component {
       </div>
     );
   }
-
 }
 
 PresentationToolbar.propTypes = {
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx
index 1fa248dc5ae578906d578a24f4a09cf72c6c683c..9722a40b6b1254c692d6f09763a76283863a8376 100644
--- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx
@@ -4,7 +4,9 @@ import { defineMessages, injectIntl, intlShape } from 'react-intl';
 import Dropzone from 'react-dropzone';
 import update from 'immutability-helper';
 import cx from 'classnames';
+import _ from 'lodash';
 
+import { notify } from '/imports/ui/services/notification';
 import ModalFullscreen from '/imports/ui/components/modal/fullscreen/component';
 import Icon from '/imports/ui/components/icon/component';
 import ButtonBase from '/imports/ui/components/button/base/component';
@@ -28,10 +30,12 @@ const propTypes = {
 };
 
 const defaultProps = {
-  defaultFileName: 'default.pdf',
 };
 
 const intlMessages = defineMessages({
+  current: {
+    id: 'app.presentationUploder.currentBadge',
+  },
   title: {
     id: 'app.presentationUploder.title',
     description: 'title of the modal',
@@ -68,6 +72,10 @@ const intlMessages = defineMessages({
     id: 'app.presentationUploder.fileToUpload',
     description: 'message used in the file selected for upload',
   },
+  genericError: {
+    id: 'app.presentationUploder.genericError',
+    description: 'generic error while uploading/converting',
+  },
   uploadProcess: {
     id: 'app.presentationUploder.upload.progress',
     description: 'message that indicates the percentage of the upload',
@@ -84,9 +92,12 @@ const intlMessages = defineMessages({
     id: 'app.presentationUploder.conversion.genericConversionStatus',
     description: 'indicates that file is being converted',
   },
+  TIMEOUT: {
+    id: 'app.presentationUploder.conversion.timeout',
+  },
   GENERATING_THUMBNAIL: {
     id: 'app.presentationUploder.conversion.generatingThumbnail',
-    description: 's that it is generating thumbnails',
+    description: 'indicatess that it is generating thumbnails',
   },
   GENERATING_SVGIMAGES: {
     id: 'app.presentationUploder.conversion.generatingSvg',
@@ -96,14 +107,21 @@ const intlMessages = defineMessages({
     id: 'app.presentationUploder.conversion.generatedSlides',
     description: 'warns that were slides generated',
   },
+  PAGE_COUNT_EXCEEDED: {
+    id: 'app.presentationUploder.conversion.pageCountExceeded',
+    description: 'warns the user that the conversion failed because of the page count',
+  },
 });
 
 class PresentationUploader extends Component {
   constructor(props) {
     super(props);
 
+    const currentPres = props.presentations.find(p => p.isCurrent);
+
     this.state = {
       presentations: props.presentations,
+      oldCurrentId: currentPres ? currentPres.id : -1,
       preventClosing: false,
       disableActions: false,
     };
@@ -118,27 +136,19 @@ class PresentationUploader extends Component {
     this.deepMergeUpdateFileKey = this.deepMergeUpdateFileKey.bind(this);
   }
 
-  componentWillReceiveProps(nextProps) {
-    const nextPresentations = nextProps.presentations;
-
-    // Update only the conversion state when receiving new props
-    nextPresentations.forEach((file) => {
-      this.updateFileKey(file.filename, 'id', file.id);
-      this.deepMergeUpdateFileKey(file.id, 'conversion', file.conversion);
-    });
-  }
-
   updateFileKey(id, key, value, operation = '$set') {
     this.setState(({ presentations }) => {
-      // Compare id and filename since non-uploaded files dont have a real id
-      const fileIndex = presentations.findIndex(f => f.id === id || f.filename === id);
+      const fileIndex = presentations.findIndex(f => f.id === id);
 
       return fileIndex === -1 ? false : {
         presentations: update(presentations, {
-          [fileIndex]: { $apply: file =>
-            update(file, { [key]: {
-              [operation]: value,
-            } }),
+          [fileIndex]: {
+            $apply: file =>
+              update(file, {
+                [key]: {
+                  [operation]: value,
+                },
+              }),
           },
         }),
       };
@@ -150,26 +160,54 @@ class PresentationUploader extends Component {
     this.updateFileKey(id, key, applyValue, '$apply');
   }
 
+  isDefault(presentation) {
+    const { defaultFileName } = this.props;
+    return presentation.filename === defaultFileName
+      && !presentation.id.includes(defaultFileName);
+  }
+
   handleConfirm() {
-    const { presentations } = this.state;
+    const presentationsToSave = this.state.presentations
+      .filter(p => !p.upload.error && !p.conversion.error);
 
     this.setState({
       disableActions: true,
       preventClosing: true,
+      presentations: presentationsToSave,
     });
 
-    return this.props.handleSave(presentations)
+    return this.props.handleSave(presentationsToSave)
       .then(() => {
+        const hasError = this.state.presentations.some(p => p.upload.error || p.conversion.error);
+        if (!hasError) {
+          this.setState({
+            disableActions: false,
+            preventClosing: false,
+          });
+
+          return;
+        }
+
+        // if theres error we dont want to close the modal
         this.setState({
           disableActions: false,
-          preventClosing: false,
+          preventClosing: true,
+        }, () => {
+          // if the selected current has error we revert back to the old one
+          const newCurrent = this.state.presentations.find(p => p.isCurrent);
+          if (newCurrent.upload.error || newCurrent.conversion.error) {
+            this.handleCurrentChange(this.state.oldCurrentId);
+          }
         });
       })
       .catch((error) => {
+        notify(this.props.intl.formatMessage(intlMessages.genericError), 'error');
+
+        console.error(error);
+
         this.setState({
           disableActions: false,
           preventClosing: true,
-          error,
         });
       });
   }
@@ -184,52 +222,62 @@ class PresentationUploader extends Component {
   }
 
   handleFiledrop(files) {
-    const presentationsToUpload = files.map(file => ({
-      file,
-      id: file.name,
-      filename: file.name,
-      isCurrent: false,
-      conversion: { done: false, error: false },
-      upload: { done: false, error: false, progress: 0 },
-      onProgress: (event) => {
-        if (!event.lengthComputable) {
-          this.deepMergeUpdateFileKey(file.name, 'upload', {
-            progress: 100,
-            done: true,
+    const presentationsToUpload = files.map((file) => {
+      const id = _.uniqueId(file.name);
+
+      return {
+        file,
+        id,
+        filename: file.name,
+        isCurrent: false,
+        conversion: { done: false, error: false },
+        upload: { done: false, error: false, progress: 0 },
+        onProgress: (event) => {
+          if (!event.lengthComputable) {
+            this.deepMergeUpdateFileKey(id, 'upload', {
+              progress: 100,
+              done: true,
+            });
+
+            return;
+          }
+
+          this.deepMergeUpdateFileKey(id, 'upload', {
+            progress: (event.loaded / event.total) * 100,
+            done: event.loaded === event.total,
           });
-
-          return;
-        }
-
-        this.deepMergeUpdateFileKey(file.name, 'upload', {
-          progress: (event.loaded / event.total) * 100,
-          done: event.loaded === event.total,
-        });
-      },
-      onError: (error) => {
-        this.deepMergeUpdateFileKey(file.name, 'upload', { error });
-      },
-    }));
+        },
+        onConversion: (conversion) => {
+          this.deepMergeUpdateFileKey(id, 'conversion', conversion);
+        },
+        onUpload: (upload) => {
+          this.deepMergeUpdateFileKey(id, 'upload', upload);
+        },
+        onDone: (newId) => {
+          this.updateFileKey(id, 'id', newId);
+        },
+      };
+    });
 
     this.setState(({ presentations }) => ({
       presentations: presentations.concat(presentationsToUpload),
     }));
   }
 
-  handleCurrentChange(item) {
+  handleCurrentChange(id) {
     const { presentations, disableActions } = this.state;
     if (disableActions) return;
 
     const currentIndex = presentations.findIndex(p => p.isCurrent);
-    const newCurrentIndex = presentations.indexOf(item);
+    const newCurrentIndex = presentations.findIndex(p => p.id === id);
 
     const commands = {};
 
     // we can end up without a current presentation
     if (currentIndex !== -1) {
       commands[currentIndex] = {
-        $apply: (_) => {
-          const p = _;
+        $apply: (presentation) => {
+          const p = presentation;
           p.isCurrent = false;
           return p;
         },
@@ -237,8 +285,8 @@ class PresentationUploader extends Component {
     }
 
     commands[newCurrentIndex] = {
-      $apply: (_) => {
-        const p = _;
+      $apply: (presentation) => {
+        const p = presentation;
         p.isCurrent = true;
         return p;
       },
@@ -256,14 +304,6 @@ class PresentationUploader extends Component {
     if (disableActions) return;
 
     const toRemoveIndex = presentations.indexOf(item);
-    const toRemove = presentations[toRemoveIndex];
-
-
-    if (toRemove.isCurrent) {
-      const defaultPresentation =
-        presentations.find(_ => _.filename === this.props.defaultFileName);
-      this.handleCurrentChange(defaultPresentation);
-    }
 
     this.setState({
       presentations: update(presentations, {
@@ -276,7 +316,17 @@ class PresentationUploader extends Component {
     const { presentations } = this.state;
 
     const presentationsSorted = presentations
-      .sort((a, b) => b.filename === this.props.defaultFileName);
+      .sort((a, b) => {
+        // Sort by ID first so files with the same name have the same order
+        if (a.id > b.id) {
+          return 1;
+        }
+        if (a.id < b.id) {
+          return -1;
+        }
+        return 0;
+      })
+      .sort((a, b) => this.isDefault(b));
 
     return (
       <div className={styles.fileList}>
@@ -303,12 +353,12 @@ class PresentationUploader extends Component {
     }
 
     if (item.upload.done && item.upload.error) {
-      const errorMessage = intlMessages[item.upload.error.code] || intlMessages.genericError;
+      const errorMessage = intlMessages[item.upload.status] || intlMessages.genericError;
       return intl.formatMessage(errorMessage);
     }
 
-    if (!item.conversion.done && item.conversion.error) {
-      const errorMessage = intlMessages[status] || intlMessages.genericError;
+    if (item.conversion.done && item.conversion.error) {
+      const errorMessage = intlMessages[item.conversion.status] || intlMessages.genericError;
       return intl.formatMessage(errorMessage);
     }
 
@@ -329,19 +379,23 @@ class PresentationUploader extends Component {
   }
 
   renderPresentationItem(item) {
-    const { disableActions } = this.state;
-
-    const isProcessing = (!item.conversion.done && item.upload.done)
-      || (!item.upload.done && item.upload.progress > 0);
-    const itemClassName = {};
-
-    itemClassName[styles.tableItemNew] = item.id === item.filename;
-    itemClassName[styles.tableItemUploading] = !item.upload.done;
-    itemClassName[styles.tableItemProcessing] = !item.conversion.done && item.upload.done;
-    itemClassName[styles.tableItemError] = item.conversion.error || item.upload.error;
-    itemClassName[styles.tableItemAnimated] = isProcessing;
+    const { disableActions, oldCurrentId } = this.state;
+
+    const isActualCurrent = item.id === oldCurrentId;
+    const isUploading = !item.upload.done && item.upload.progress > 0;
+    const isConverting = !item.conversion.done && item.upload.done;
+    const hasError = item.conversion.error || item.upload.error;
+    const isProcessing = (isUploading || isConverting) && !hasError;
+
+    const itemClassName = {
+      [styles.tableItemNew]: item.id.indexOf(item.filename) !== -1,
+      [styles.tableItemUploading]: isUploading,
+      [styles.tableItemConverting]: isConverting,
+      [styles.tableItemError]: hasError,
+      [styles.tableItemAnimated]: isProcessing,
+    };
 
-    const hideRemove = isProcessing || item.filename === this.props.defaultFileName;
+    const hideRemove = this.isDefault(item);
 
     return (
       <tr
@@ -349,33 +403,44 @@ class PresentationUploader extends Component {
         className={cx(itemClassName)}
       >
         <td className={styles.tableItemIcon}>
-          <Icon iconName={'file'} />
+          <Icon iconName="file" />
         </td>
-        <th className={styles.tableItemName}>
+        {
+          isActualCurrent ?
+            <th className={styles.tableItemCurrent}>
+              <span className={styles.currentLabel}>
+                {this.props.intl.formatMessage(intlMessages.current)}
+              </span>
+            </th>
+          : null
+        }
+        <th className={styles.tableItemName} colSpan={!isActualCurrent ? 2 : 0}>
           <span>{item.filename}</span>
         </th>
-        <td className={styles.tableItemStatus}>
+        <td className={styles.tableItemStatus} colSpan={hasError ? 2 : 0}>
           {this.renderPresentationItemStatus(item)}
         </td>
-        <td className={styles.tableItemActions}>
-          <Checkbox
-            disabled={disableActions}
-            ariaLabel={'Set as current presentation'}
-            className={styles.itemAction}
-            checked={item.isCurrent}
-            onChange={() => this.handleCurrentChange(item)}
-          />
-          { hideRemove ? null : (
-            <ButtonBase
+        { hasError ? null : (
+          <td className={styles.tableItemActions}>
+            <Checkbox
               disabled={disableActions}
-              className={cx(styles.itemAction, styles.itemActionRemove)}
-              label={'Remove presentation'}
-              onClick={() => this.handleRemove(item)}
-            >
-              <Icon iconName={'delete'} />
-            </ButtonBase>
-          )}
-        </td>
+              ariaLabel="Set as current presentation"
+              className={styles.itemAction}
+              checked={item.isCurrent}
+              onChange={() => this.handleCurrentChange(item.id)}
+            />
+            { hideRemove ? null : (
+              <ButtonBase
+                disabled={disableActions}
+                className={cx(styles.itemAction, styles.itemActionRemove)}
+                label="Remove presentation"
+                onClick={() => this.handleRemove(item)}
+              >
+                <Icon iconName="delete" />
+              </ButtonBase>
+            )}
+          </td>
+        )}
       </tr>
     );
   }
@@ -404,7 +469,7 @@ class PresentationUploader extends Component {
         disablePreview
         onDrop={this.handleFiledrop}
       >
-        <Icon className={styles.dropzoneIcon} iconName={'upload'} />
+        <Icon className={styles.dropzoneIcon} iconName="upload" />
         <p className={styles.dropzoneMessage}>
           {intl.formatMessage(intlMessages.dropzoneLabel)}&nbsp;
           <span className={styles.dropzoneLink}>
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js
index 54daf9c1b614664950a4a1c5a2a6d3603d6aadb3..763530a5d48f806100c5f3e1897556fb01b943fe 100644
--- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js
+++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js
@@ -1,6 +1,7 @@
 import Presentations from '/imports/api/presentations';
 import Auth from '/imports/ui/services/auth';
 import { makeCall } from '/imports/ui/services/api';
+import _ from 'lodash';
 
 const CONVERSION_TIMEOUT = 300000;
 
@@ -29,7 +30,9 @@ const futch = (url, opts = {}, onProgress) => new Promise((res, rej) => {
 
 const getPresentations = () =>
   Presentations
-    .find()
+    .find({
+      'conversion.error': false,
+    })
     .fetch()
     .map(presentation => ({
       id: presentation.id,
@@ -39,36 +42,40 @@ const getPresentations = () =>
       conversion: presentation.conversion || { done: true, error: false },
     }));
 
-const observePresentationConversion = (meetingId, filename) => new Promise((resolve, reject) => {
-  const conversionTimeout = setTimeout(() => {
-    reject({
-      filename,
-      message: 'Conversion timeout.',
-    });
-  }, CONVERSION_TIMEOUT);
-
-  const didValidate = (doc) => {
-    clearTimeout(conversionTimeout);
-    resolve(doc);
-  };
-
-  Tracker.autorun((c) => {
-    /* FIXME: With two presentations with the same name this will not work as expected */
-    const query = Presentations.find({ meetingId });
-
-    query.observe({
-      changed: (newDoc) => {
-        if (newDoc.name !== filename) return;
-        if (newDoc.conversion.done) {
-          c.stop();
-          didValidate(newDoc);
-        }
-      },
+const observePresentationConversion = (meetingId, filename, onConversion) =>
+  new Promise((resolve) => {
+    const conversionTimeout = setTimeout(() => {
+      onConversion({
+        done: true,
+        error: true,
+        status: 'TIMEOUT',
+      });
+    }, CONVERSION_TIMEOUT);
+
+    const didValidate = (doc) => {
+      clearTimeout(conversionTimeout);
+      resolve(doc);
+    };
+
+    Tracker.autorun((c) => {
+      const query = Presentations.find({ meetingId });
+
+      query.observe({
+        changed: (newDoc) => {
+          if (newDoc.name !== filename) return;
+
+          onConversion(newDoc.conversion);
+
+          if (newDoc.conversion.done) {
+            c.stop();
+            didValidate(newDoc);
+          }
+        },
+      });
     });
   });
-});
 
-const uploadAndConvertPresentation = (file, meetingID, endpoint, onError, onProgress) => {
+const uploadAndConvertPresentation = (file, meetingID, endpoint, onUpload, onProgress, onConversion) => {
   const data = new FormData();
   data.append('presentation_name', file.name);
   data.append('Filename', file.name);
@@ -84,19 +91,17 @@ const uploadAndConvertPresentation = (file, meetingID, endpoint, onError, onProg
   };
 
   return futch(endpoint, opts, onProgress)
-    .then(() => observePresentationConversion(meetingID, file.name))
+    .then(() => observePresentationConversion(meetingID, file.name, onConversion))
     // Trap the error so we can have parallel upload
     .catch((error) => {
-      onError(error);
-      return observePresentationConversion(meetingID, file.name);
+      onUpload({ error: true, done: true, status: error.code });
+      return Promise.resolve();
     });
 };
 
 const uploadAndConvertPresentations = (presentationsToUpload, meetingID, uploadEndpoint) =>
-  Promise.all(
-    presentationsToUpload.map(p =>
-      uploadAndConvertPresentation(p.file, meetingID, uploadEndpoint, p.onError, p.onProgress)),
-  );
+  Promise.all(presentationsToUpload.map(p =>
+    uploadAndConvertPresentation(p.file, meetingID, uploadEndpoint, p.onUpload, p.onProgress, p.onConversion)));
 
 const setPresentation = presentationID => makeCall('setPresentation', presentationID);
 
@@ -106,27 +111,42 @@ const removePresentations = presentationsToRemove =>
   Promise.all(presentationsToRemove.map(p => removePresentation(p.id)));
 
 const persistPresentationChanges = (oldState, newState, uploadEndpoint) => {
-  const presentationsToUpload = newState.filter(_ => !oldState.includes(_));
-  const presentationsToRemove = oldState.filter(_ => !newState.includes(_));
-  const currentPresentation = newState.find(_ => _.isCurrent);
-
-  return new Promise((resolve, reject) =>
-    uploadAndConvertPresentations(presentationsToUpload, Auth.meetingID, uploadEndpoint)
-      .then((presentations) => {
-        if (!presentations.length && !currentPresentation) return Promise.resolve();
-
-        // If its a newly uploaded presentation we need to get its id from promise result
-        const currentPresentationId =
-          currentPresentation.id !== currentPresentation.filename ?
-          currentPresentation.id :
-          presentations[presentationsToUpload.findIndex(_ => _ === currentPresentation)].id;
-
-        return setPresentation(currentPresentationId);
-      })
-      .then(removePresentations.bind(null, presentationsToRemove))
-      .then(resolve)
-      .catch(reject),
-  );
+  const presentationsToUpload = newState.filter(p => !p.upload.done);
+  const presentationsToRemove = oldState.filter(p => !_.find(newState, ['id', p.id]));
+
+  let currentPresentation = newState.find(p => p.isCurrent);
+
+  return uploadAndConvertPresentations(presentationsToUpload, Auth.meetingID, uploadEndpoint)
+    .then((presentations) => {
+      if (!presentations.length && !currentPresentation) return Promise.resolve();
+
+      // Update the presentation with their new ids
+      presentations.forEach((p, i) => {
+        if (p === undefined) return;
+        presentationsToUpload[i].onDone(p.id);
+      });
+
+      return Promise.resolve(presentations);
+    })
+    .then((presentations) => {
+      if (currentPresentation === undefined) {
+        return Promise.resolve();
+      }
+
+      // If its a newly uploaded presentation we need to get it from promise result
+      if (!currentPresentation.conversion.done) {
+        const currentIndex = presentationsToUpload.findIndex(p => p === currentPresentation);
+        currentPresentation = presentations[currentIndex];
+      }
+
+      // skip setting as current if error happened
+      if (currentPresentation.conversion.error) {
+        return Promise.resolve();
+      }
+
+      return setPresentation(currentPresentation.id);
+    })
+    .then(removePresentations.bind(null, presentationsToRemove));
 };
 
 export default {
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/styles.scss b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/styles.scss
index 9d240b7f072e892204f64fd52b55df0285d9d0a5..275aa3218cd33f5a170dc8acf17e5cf64b082a66 100644
--- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/styles.scss
@@ -57,12 +57,14 @@ $item-height: 1rem;
 
 .tableItemIcon,
 .tableItemActions,
-.tableItemStatus {
+.tableItemStatus,
+.tableItemCurrent {
   width: 1%;
 }
 
 .tableItemActions {
   min-width: 68px; // size of the 2 icons (check/trash)
+  text-align: right;
 }
 
 .tableItemIcon > i {
@@ -75,7 +77,7 @@ $item-height: 1rem;
   position: relative;
 
   &:before {
-    content: '&nbsp;';
+    content: "\00a0";
     visibility: hidden;
   }
 
@@ -84,10 +86,13 @@ $item-height: 1rem;
     position: absolute;
     left: 0;
     right: 0;
-    padding: 0 $sm-padding-x;
   }
 }
 
+.tableItemCurrent {
+  padding-left: 0;
+}
+
 .tableItemStatus {
   text-align: right;
 }
@@ -100,7 +105,7 @@ $item-height: 1rem;
   background-color: transparentize($color-primary, .75);
 }
 
-.tableItemProcessing {
+.tableItemConverting {
   background-color: transparentize($color-success, .75);
 }
 
@@ -183,3 +188,18 @@ $item-height: 1rem;
   font-size: 80%;
   display: block;
 }
+
+.currentLabel {
+  display: inline;
+  padding: .25em .5em;
+  font-size: 75%;
+  font-weight: 700;
+  line-height: 1;
+  color: $color-white;
+  background: $color-primary;
+  text-align: center;
+  white-space: nowrap;
+  vertical-align: baseline;
+  border-radius: .25em;
+  text-transform: uppercase;
+}
diff --git a/bigbluebutton-html5/imports/ui/components/settings/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/component.jsx
index abcd6879bc5ea41851c32027b4571384e336520d..f0f50d876667d3d262e60382e1ca93968f60ce8a 100644
--- a/bigbluebutton-html5/imports/ui/components/settings/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/settings/component.jsx
@@ -1,13 +1,13 @@
 import React, { Component } from 'react';
 import Modal from '/imports/ui/components/modal/fullscreen/component';
 import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
-import { defineMessages, injectIntl } from 'react-intl';
+import { defineMessages, injectIntl, intlShape } from 'react-intl';
 import ClosedCaptions from '/imports/ui/components/settings/submenus/closed-captions/component';
 import Application from '/imports/ui/components/settings/submenus/application/container';
-import Participants from '/imports/ui/components/settings/submenus/participants/component';
 import _ from 'lodash';
-import { withModalMounter } from '../modal/service';
+import PropTypes from 'prop-types';
 
+import { withModalMounter } from '../modal/service';
 import Icon from '../icon/component';
 import styles from './styles';
 
@@ -55,6 +55,15 @@ const intlMessages = defineMessages({
 });
 
 const propTypes = {
+  intl: intlShape.isRequired,
+  video: PropTypes.object.isRequired,
+  application: PropTypes.object.isRequired,
+  cc: PropTypes.object.isRequired,
+  participants: PropTypes.object.isRequired,
+  updateSettings: PropTypes.func.isRequired,
+  availableLocales: PropTypes.object.isRequired,
+  mountModal: PropTypes.func.isRequired,
+  locales: PropTypes.array.isRequired,
 };
 
 class Settings extends Component {
@@ -110,7 +119,6 @@ class Settings extends Component {
 
   renderModalContent() {
     const {
-      isModerator,
       intl,
     } = this.props;
 
@@ -134,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
@@ -161,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>
     );
   }
@@ -198,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 b79dc2f99be3896dfbb40bdd9ff491d26ec91a9c..145dc92c8b58136de4f6e3723445b83d477ad5c1 100644
--- a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx
@@ -1,8 +1,5 @@
 import React from 'react';
-import Modal from 'react-modal';
-import Icon from '/imports/ui/components/icon/component';
 import Button from '/imports/ui/components/button/component';
-import ReactDOM from 'react-dom';
 import cx from 'classnames';
 import Toggle from '/imports/ui/components/switch/component';
 import { defineMessages, injectIntl } from 'react-intl';
@@ -149,13 +146,13 @@ 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>
             </div>
           </div>
-          {/* TODO: Uncomment after the release
+
           <div className={styles.row}>
             <div className={styles.col}>
               <div className={styles.formElement} >
@@ -170,12 +167,12 @@ 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>
             </div>
-          </div>*/}
+          </div>
           <div className={styles.row}>
             <div className={styles.col}>
               <div className={styles.formElement}>
@@ -185,29 +182,27 @@ class ApplicationMenu extends BaseMenu {
               </div>
             </div>
             <div className={styles.col}>
-                <label aria-labelledby="changeLangLabel" className={cx(styles.formElement, styles.pullContentRight)}>
+              <label aria-labelledby="changeLangLabel" className={cx(styles.formElement, styles.pullContentRight)}>
+      { availableLocales && availableLocales.length > 0 ?
                   <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) }
+                      { intl.formatMessage(intlMessages.languageOptionLabel) }
                     </option>
-                    {availableLocales ? availableLocales.map((locale, index) =>
-                    (<option key={index} value={locale.locale}>
-                      {locale.name}
-                    </option>),
-                  ) : null }
+                    { availableLocales.map((locale, index) =>
+                      (<option key={index} value={locale.locale}>
+                        {locale.name}
+                      </option>)) }
                   </select>
-                </label>
-                <div
-                  id="changeLangLabel"
-                  aria-label={intl.formatMessage(intlMessages.ariaLanguageLabel)}
-                />
+                : null }
+              </label>
+              <div
+                id="changeLangLabel"
+                aria-label={intl.formatMessage(intlMessages.ariaLanguageLabel)}
+              />
             </div>
           </div>
           <hr className={styles.separator} />
@@ -232,8 +227,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)}
@@ -242,8 +237,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/settings/submenus/styles.scss b/bigbluebutton-html5/imports/ui/components/settings/submenus/styles.scss
index 3bbb7cd481e1c94e98254083b8c6ac50ece499a4..69d5bc16fb3e4e4a874c219287c8a55d7fba670e 100644
--- a/bigbluebutton-html5/imports/ui/components/settings/submenus/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/settings/submenus/styles.scss
@@ -1,3 +1,4 @@
+@import "/imports/ui/stylesheets/mixins/focus";
 @import "/imports/ui/stylesheets/variables/_all";
 
 .title {
@@ -53,10 +54,11 @@
 }
 
 .select {
-  @extend %customSelectFocus;
-  background-color: #fff;
-  border: $focus-border-size solid #fff;
-  border-radius: $focus-border-size;
+  @include elementFocus($color-primary);
+
+  background-color: $color-white;
+  border: $border-size solid $color-white;
+  border-radius: $border-size;
   border-bottom: 0.1rem solid $color-gray-lighter;
   color: $color-gray-label;
   width: 100%;
@@ -91,12 +93,13 @@
 }
 
 .swatch {
-  @extend %customSelectFocus;
+  @include elementFocus($color-primary);
+
   position: absolute;
   float: right;
-  background: #fff;
-  border-radius: 5px;
-  border: 2px solid $color-gray-light;
+  background: $color-white;
+  border-radius: $border-size;
+  border: $border-size solid $color-gray-light;
   display: inline-block;
   cursor: pointer;
 }
@@ -104,7 +107,7 @@
 .swatchInner {
   width: 3rem;
   height: 1.1rem;
-  border-radius: $focus-border-size;
+  border-radius: $border-size;
 }
 
 .colorPickerOverlay {
diff --git a/bigbluebutton-html5/imports/ui/components/tooltip/component.jsx b/bigbluebutton-html5/imports/ui/components/tooltip/component.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..dd8b694ec96427cc22b0995c042313308379da71
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/tooltip/component.jsx
@@ -0,0 +1,86 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Tippy from 'tippy.js';
+import _ from 'lodash';
+import cx from 'classnames';
+import { ESCAPE } from '/imports/utils/keyCodes';
+
+const propTypes = {
+  title: PropTypes.string.isRequired,
+  position: PropTypes.oneOf(['bottom']),
+  children: PropTypes.element.isRequired,
+  className: PropTypes.string,
+};
+
+const defaultProps = {
+  position: 'bottom',
+  className: null,
+};
+
+class Tooltip extends Component {
+  constructor(props) {
+    super(props);
+
+    this.tippySelectorId = _.uniqueId('tippy-');
+    this.onShow = this.onShow.bind(this);
+    this.onHide = this.onHide.bind(this);
+    this.handleEscapeHide = this.handleEscapeHide.bind(this);
+    this.delay = [250, 100];
+    this.dynamicTitle = true;
+  }
+
+  componentDidMount() {
+    const {
+      position,
+    } = this.props;
+
+    const options = {
+      position,
+      dynamicTitle: this.dynamicTitle,
+      delay: this.delay,
+      onShow: this.onShow,
+      onHide: this.onHide,
+    };
+
+    this.tooltip = Tippy(`#${this.tippySelectorId}`, options);
+  }
+
+  onShow() {
+    document.addEventListener('keyup', this.handleEscapeHide);
+  }
+
+  onHide() {
+    document.removeEventListener('keyup', this.handleEscapeHide);
+  }
+
+  handleEscapeHide(e) {
+    if (e.keyCode !== ESCAPE) return;
+
+    this.tooltip.tooltips[0].hide();
+  }
+
+  render() {
+    const {
+      children,
+      className,
+      title,
+      ...restProps
+    } = this.props;
+
+    const WrappedComponent = React.Children.only(children);
+
+    const WrappedComponentBound = React.cloneElement(WrappedComponent, {
+      ...restProps,
+      title,
+      id: this.tippySelectorId,
+      className: cx(children.props.className, className),
+    });
+
+    return WrappedComponentBound;
+  }
+}
+
+export default Tooltip;
+
+Tooltip.defaultProps = defaultProps;
+Tooltip.propTypes = propTypes;
diff --git a/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss b/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss
index c636ab4f96c6dbec45fdf3c9fa89e8ab6350d89b..e5df4bf31b81cabe14e6b9dfcce6d0838e2a604d 100644
--- a/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss
@@ -9,7 +9,7 @@ $user-indicators-offset: -5px;
 $user-indicator-presenter-bg: $color-primary;
 $user-indicator-voice-bg: $color-success;
 $user-indicator-muted-bg: $color-danger;
-$user-list-bg: #F3F6F9;
+$user-list-bg: $color-off-white;
 
 .avatar {
   position: relative;
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/chat-list-item/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/styles.scss
index caac495cda7d2dcabaab84eec44b78531e87fb16..00a15d7454e10e8aa607d0bef56893cbfaeb5cb2 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/user-list/chat-list-item/styles.scss
@@ -1,7 +1,7 @@
 @import "../styles.scss";
+@import "/imports/ui/stylesheets/variables/_all";
 
 .chatListItem {
-  @extend %customChatItemFocus;
   @extend %list-item;
   cursor: pointer;
   padding: 0;
@@ -42,8 +42,12 @@
   font-size: 0.9rem;
   font-weight: 400;
   margin-left: $sm-padding-x;
+  position: relative;
+  top: $md-padding-y;
 }
 
 .active {
   background-color: $list-item-bg-hover;
+  box-shadow: inset 0 0 0 $border-size $item-focus-border, inset 1px 0 0 1px $item-focus-border;
+  outline: none;
 }
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx
index 5b4d61c507fa8c0795fd7b01608f9f3cd436bbba..eec083ad33ee82fb9c0f726dd666ec3bee36e768 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx
@@ -5,7 +5,6 @@ import { withRouter } from 'react-router';
 
 import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
 import styles from './styles';
-import UserListHeader from './user-list-header/component';
 import UserContent from './user-list-content/component';
 
 const propTypes = {
@@ -27,6 +26,7 @@ const propTypes = {
   kickUser: PropTypes.func.isRequired,
   toggleVoice: PropTypes.func.isRequired,
   changeRole: PropTypes.func.isRequired,
+  roving: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
@@ -54,10 +54,6 @@ class UserList extends Component {
   render() {
     return (
       <div className={styles.userList}>
-        {/* <UserListHeader
-          intl={this.props.intl}
-          compact={this.state.compact}
-        /> */}
         {<UserContent
           intl={this.props.intl}
           openChats={this.props.openChats}
@@ -75,6 +71,7 @@ class UserList extends Component {
           normalizeEmojiName={this.props.normalizeEmojiName}
           isMeetingLocked={this.props.isMeetingLocked}
           isPublicChat={this.props.isPublicChat}
+          roving={this.props.roving}
         />}
       </div>
     );
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx
index eff13f2dd01100f5cf7533c6c29fa40a7b9cc26e..4d8063c9ed63e075673e283c6919bab81466a4df 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx
@@ -1,10 +1,36 @@
 import React from 'react';
+import PropTypes from 'prop-types';
 import { createContainer } from 'meteor/react-meteor-data';
 import { meetingIsBreakout } from '/imports/ui/components/app/service';
 import Meetings from '/imports/api/meetings';
 import Service from './service';
 import UserList from './component';
 
+const propTypes = {
+  openChats: PropTypes.arrayOf(String).isRequired,
+  openChat: PropTypes.string.isRequired,
+  users: PropTypes.arrayOf(Object).isRequired,
+  currentUser: PropTypes.shape({}).isRequired,
+  meeting: PropTypes.shape({}).isRequired,
+  isBreakoutRoom: PropTypes.bool.isRequired,
+  getAvailableActions: PropTypes.func.isRequired,
+  normalizeEmojiName: PropTypes.func.isRequired,
+  isMeetingLocked: PropTypes.func.isRequired,
+  isPublicChat: PropTypes.func.isRequired,
+  setEmojiStatus: PropTypes.func.isRequired,
+  assignPresenter: PropTypes.func.isRequired,
+  kickUser: PropTypes.func.isRequired,
+  toggleVoice: PropTypes.func.isRequired,
+  changeRole: PropTypes.func.isRequired,
+  roving: PropTypes.func.isRequired,
+  userActions: PropTypes.func.isRequired,
+  children: PropTypes.Object,
+};
+
+const defaultProps = {
+  children: {},
+};
+
 const UserListContainer = (props) => {
   const {
     users,
@@ -24,7 +50,8 @@ const UserListContainer = (props) => {
     kickUser,
     toggleVoice,
     changeRole,
-    } = props;
+    roving,
+  } = props;
 
   return (
     <UserList
@@ -44,12 +71,16 @@ const UserListContainer = (props) => {
       normalizeEmojiName={normalizeEmojiName}
       isMeetingLocked={isMeetingLocked}
       isPublicChat={isPublicChat}
+      roving={roving}
     >
       {children}
     </UserList>
   );
 };
 
+UserListContainer.propTypes = propTypes;
+UserListContainer.defaultProps = defaultProps;
+
 export default createContainer(({ params }) => ({
   users: Service.getUsers(),
   meeting: Meetings.findOne({}),
@@ -67,4 +98,5 @@ export default createContainer(({ params }) => ({
   kickUser: Service.kickUser,
   toggleVoice: Service.toggleVoice,
   changeRole: Service.changeRole,
+  roving: Service.roving,
 }), UserListContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js
index 9fb0c0d2ad773c3c4082b1560841107d37f1c15e..1116627ff613bc3a08bacf591457e32fcace4a58 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/service.js
+++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js
@@ -5,9 +5,13 @@ 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';
+import KEY_CODES from '/imports/utils/keyCodes';
+
+const APP_CONFIG = Meteor.settings.public.app;
+const ALLOW_MODERATOR_TO_UNMUTE_AUDIO = APP_CONFIG.allowModeratorToUnmuteAudio;
 
 const CHAT_CONFIG = Meteor.settings.public.chat;
 const PRIVATE_CHAT_TYPE = CHAT_CONFIG.type_private;
@@ -38,18 +42,21 @@ const sortUsersByName = (a, b) => {
 };
 
 const sortUsersByEmoji = (a, b) => {
-  const emojiA = a in EMOJI_STATUSES ? EMOJI_STATUSES[a] : a;
-  const emojiB = b in EMOJI_STATUSES ? EMOJI_STATUSES[b] : b;
+  const { status: statusA } = a.emoji;
+  const { status: statusB } = b.emoji;
+
+  const emojiA = statusA in EMOJI_STATUSES ? EMOJI_STATUSES[statusA] : statusA;
+  const emojiB = statusB in EMOJI_STATUSES ? EMOJI_STATUSES[statusB] : statusB;
 
-  if (emojiA && emojiB) {
+  if (emojiA && emojiB && (emojiA !== EMOJI_STATUSES.none && emojiB !== EMOJI_STATUSES.none)) {
     if (a.emoji.changedAt < b.emoji.changedAt) {
       return -1;
     } else if (a.emoji.changedAt > b.emoji.changedAt) {
       return 1;
     }
-  } else if (emojiA) {
+  } else if (emojiA && emojiA !== EMOJI_STATUSES.none) {
     return -1;
-  } else if (emojiB) {
+  } else if (emojiB && emojiB !== EMOJI_STATUSES.none) {
     return 1;
   }
   return 0;
@@ -69,7 +76,7 @@ const sortUsersByModerator = (a, b) => {
 
 const sortUsersByPhoneUser = (a, b) => {
   if (!a.isPhoneUser && !b.isPhoneUser) {
-    return sortUsersByName(a, b);
+    return 0;
   } else if (!a.isPhoneUser) {
     return -1;
   } else if (!b.isPhoneUser) {
@@ -208,20 +215,46 @@ const getOpenChats = (chatID) => {
     .sort(sortChats);
 };
 
+const isVoiceOnlyUser = userId => userId.toString().startsWith('v_');
+
 const getAvailableActions = (currentUser, user, router, isBreakoutRoom) => {
+  const isDialInUser = isVoiceOnlyUser(user.id) || user.isPhoneUser;
+
   const hasAuthority = currentUser.isModerator || user.isCurrent;
-  const allowedToChatPrivately = !user.isCurrent;
-  const allowedToMuteAudio = hasAuthority && user.isVoiceUser && !user.isMuted;
-  const allowedToUnmuteAudio = hasAuthority && user.isVoiceUser && user.isMuted;
-  const allowedToResetStatus = hasAuthority && user.emoji.status !== EMOJI_STATUSES.none;
+
+  const allowedToChatPrivately = !user.isCurrent && !isDialInUser;
+
+  const allowedToMuteAudio = hasAuthority
+                            && user.isVoiceUser
+                            && !user.isMuted
+                            && !user.isListenOnly;
+
+  const allowedToUnmuteAudio = hasAuthority
+                              && user.isVoiceUser
+                              && !user.isListenOnly
+                              && user.isMuted
+                              && (ALLOW_MODERATOR_TO_UNMUTE_AUDIO || user.isCurrent);
+
+  const allowedToResetStatus = hasAuthority
+      && user.emoji.status !== EMOJI_STATUSES.none
+      && !isDialInUser;
 
   // if currentUser is a moderator, allow kicking other users
   const allowedToKick = currentUser.isModerator && !user.isCurrent && !isBreakoutRoom;
 
-  const allowedToSetPresenter = currentUser.isModerator && !user.isPresenter;
+  const allowedToSetPresenter = currentUser.isModerator
+      && !user.isPresenter
+      && !isDialInUser;
+
+  const allowedToPromote = currentUser.isModerator
+      && !user.isCurrent
+      && !user.isModerator
+      && !isDialInUser;
 
-  const allowedToPromote = currentUser.isModerator && !user.isCurrent && !user.isModerator;
-  const allowedToDemote = currentUser.isModerator && !user.isCurrent && user.isModerator;
+  const allowedToDemote = currentUser.isModerator
+      && !user.isCurrent
+      && user.isModerator
+      && !isDialInUser;
 
   return {
     allowedToChatPrivately,
@@ -243,7 +276,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) => {
@@ -268,12 +301,54 @@ const setEmojiStatus = (userId) => { makeCall('setEmojiStatus', userId, 'none');
 
 const assignPresenter = (userId) => { makeCall('assignPresenter', userId); };
 
-const kickUser = (userId) => { makeCall('kickUser', userId); };
+const kickUser = (userId) => {
+  if (isVoiceOnlyUser(userId)) {
+    makeCall('ejectUserFromVoice', userId);
+  } else {
+    makeCall('kickUser', userId);
+  }
+};
 
 const toggleVoice = (userId) => { makeCall('toggleVoice', userId); };
 
 const changeRole = (userId, role) => { makeCall('changeRole', userId, role); };
 
+const roving = (event, itemCount, changeState) => {
+  if (this.selectedIndex === undefined) {
+    this.selectedIndex = -1;
+  }
+
+  if ([KEY_CODES.ESCAPE, KEY_CODES.TAB].includes(event.keyCode)) {
+    document.activeElement.blur();
+    this.selectedIndex = -1;
+    changeState(this.selectedIndex);
+  }
+
+  if (event.keyCode === KEY_CODES.ARROW_DOWN) {
+    this.selectedIndex += 1;
+
+    if (this.selectedIndex === itemCount) {
+      this.selectedIndex = 0;
+    }
+
+    changeState(this.selectedIndex);
+  }
+
+  if (event.keyCode === KEY_CODES.ARROW_UP) {
+    this.selectedIndex -= 1;
+
+    if (this.selectedIndex < 0) {
+      this.selectedIndex = itemCount - 1;
+    }
+
+    changeState(this.selectedIndex);
+  }
+
+  if ([KEY_CODES.ARROW_RIGHT, KEY_CODES.SPACE].includes(event.keyCode)) {
+    document.activeElement.firstChild.click();
+  }
+};
+
 export default {
   setEmojiStatus,
   assignPresenter,
@@ -287,4 +362,5 @@ export default {
   normalizeEmojiName,
   isMeetingLocked,
   isPublicChat,
+  roving,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/styles.scss
index 3eed73e6cb969e5ec211aca01cc1534b29873069..8fb6e671c49065ac3be02c10da6adc5e47662956 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/user-list/styles.scss
@@ -7,7 +7,7 @@
 
 $unread-messages-bg: $color-danger;
 $user-list-text: $color-gray;
-$user-list-bg: #F3F6F9;
+$user-list-bg: $color-off-white;
 
 $user-thumbnail-border: $color-gray-light;
 $user-thumbnail-text: $user-thumbnail-border;
@@ -23,6 +23,8 @@ $sub-name-color: $color-gray-light;
 $user-icons-color: $color-gray-light;
 $user-icons-color-hover: $color-gray;
 
+$list-item-bg-hover: darken($color-off-white, 7%);
+$item-focus-border: $color-blue-lighter;
 
 /* classes for extending
  * ==========
@@ -39,9 +41,6 @@ $user-icons-color-hover: $color-gray;
 %list-item {
   display: flex;
   flex-flow: row;
-  padding-top: $sm-padding-y;
-  padding-bottom: $sm-padding-y;
-  margin-top: $sm-padding-y * 2;
   transition: all 0.3s;
   border-top-left-radius: 5px;
   border-bottom-left-radius: 5px;
@@ -50,6 +49,17 @@ $user-icons-color-hover: $color-gray;
   &:first-child {
     margin-top: 0;
   }
+
+  &:focus {
+    background-color: $list-item-bg-hover;
+    box-shadow: inset 0 0 0 $border-size $item-focus-border, inset 1px 0 0 1px $item-focus-border;
+    outline: none;
+  }
+
+  &:hover {
+    background-color: $list-item-bg-hover;
+  }
+
 }
 
 /* Styling
@@ -103,7 +113,7 @@ $user-icons-color-hover: $color-gray;
   font-weight: 600;
   text-transform: uppercase;
   padding: 0 $sm-padding-x;
-  margin: ($md-padding-x / 2) 0 0 0;
+  margin: 0 0 ($lg-padding-x / 2) 0;
   color: $color-gray-light;
 }
 
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx
index 4d701358e8e199b6c816d33a30f0d24c344e6ea0..279222ea48a60224d489f846a18cd3fb5f8ab7e2 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx
@@ -1,6 +1,5 @@
 import React, { Component } from 'react';
 import PropTypes from 'prop-types';
-import KEY_CODES from '/imports/utils/keyCodes';
 import styles from './styles';
 import UserParticipants from './user-participants/component';
 import UserMessages from './user-messages/component';
@@ -24,6 +23,7 @@ const propTypes = {
   kickUser: PropTypes.func.isRequired,
   toggleVoice: PropTypes.func.isRequired,
   changeRole: PropTypes.func.isRequired,
+  roving: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
@@ -35,82 +35,6 @@ const defaultProps = {
 };
 
 class UserContent extends Component {
-
-  static focusElement(active, element) {
-    const modifiedActive = active;
-    const modifiedElement = element;
-    if (!modifiedActive.getAttribute('role') === 'tabpanel') {
-      modifiedActive.tabIndex = -1;
-    }
-    modifiedElement.tabIndex = 0;
-    modifiedElement.focus();
-  }
-
-  static removeFocusFromChildren(children, numberOfItems) {
-    const modifiedChildren = children;
-    for (let i = 0; i < numberOfItems; i += 1) {
-      modifiedChildren.childNodes[i].tabIndex = -1;
-    }
-  }
-
-  constructor(props) {
-    super(props);
-
-    this.rovingIndex = this.rovingIndex.bind(this);
-    this.focusList = this.focusList.bind(this);
-    this.focusedItemIndex = -1;
-  }
-
-  focusList(list) {
-    const focusList = list;
-    document.activeElement.tabIndex = -1;
-    this.focusedItemIndex = -1;
-    focusList.tabIndex = 0;
-    focusList.focus();
-  }
-
-
-  rovingIndex(event, list, items, numberOfItems) {
-    const active = document.activeElement;
-    const changedItems = items;
-
-    if (event.keyCode === KEY_CODES.TAB) {
-      if (this.focusedItemIndex !== -1) {
-        this.focusedItemIndex = 0;
-        UserContent.removeFocusFromChildren(changedItems, numberOfItems);
-      }
-    }
-
-    if (event.keyCode === KEY_CODES.ESCAPE
-      || this.focusedItemIndex < 0
-      || this.focusedItemIndex > numberOfItems) {
-      this.focusList(list);
-    }
-
-    if ([KEY_CODES.ARROW_RIGHT, KEY_CODES.ARROW_SPACE].includes(event.keyCode)) {
-      active.firstChild.click();
-    }
-
-    if (event.keyCode === KEY_CODES.ARROW_DOWN) {
-      this.focusedItemIndex += 1;
-
-      if (this.focusedItemIndex === numberOfItems) {
-        this.focusedItemIndex = 0;
-      }
-      UserContent.focusElement(active, changedItems.childNodes[this.focusedItemIndex]);
-    }
-
-    if (event.keyCode === KEY_CODES.ARROW_UP) {
-      this.focusedItemIndex -= 1;
-
-      if (this.focusedItemIndex < 0) {
-        this.focusedItemIndex = numberOfItems - 1;
-      }
-
-      UserContent.focusElement(active, changedItems.childNodes[this.focusedItemIndex]);
-    }
-  }
-
   render() {
     return (
       <div className={styles.content}>
@@ -119,7 +43,7 @@ class UserContent extends Component {
           openChats={this.props.openChats}
           compact={this.props.compact}
           intl={this.props.intl}
-          rovingIndex={this.rovingIndex}
+          roving={this.props.roving}
         />
         <UserParticipants
           users={this.props.users}
@@ -135,8 +59,8 @@ class UserContent extends Component {
           changeRole={this.props.changeRole}
           getAvailableActions={this.props.getAvailableActions}
           normalizeEmojiName={this.props.normalizeEmojiName}
-          rovingIndex={this.rovingIndex}
           isMeetingLocked={this.props.isMeetingLocked}
+          roving={this.props.roving}
         />
       </div>
     );
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 ca81abc1ee0bd69435145c6134dfae12bd1277ac..e114f61446facb5a5271b0f878abf7b369e1bcd3 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
@@ -1,5 +1,7 @@
 @import "/imports/ui/components/user-list/styles.scss";
 @import "/imports/ui/stylesheets/mixins/_scrollable";
+@import "/imports/ui/stylesheets/mixins/focus";
+@import "/imports/ui/stylesheets/variables/_all";
 
 .content {
   @extend %flex-column;
@@ -8,6 +10,33 @@
 }
 
 .scrollableList {
-  @extend %customListFocus;
+  @include elementFocus($list-item-bg-hover);
   @include scrollbox-vertical($user-list-bg);
+
+  &:active {
+    box-shadow: none;
+    border-radius: none;
+  }
+}
+
+.list {
+  margin-left: $md-padding-y;
+  margin-bottom: 1px;
+}
+
+.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 2a51b006d89904c4924ca885d3cdc6d65865aa64..9b6aa43332791bd179d7e6ecf07ed4edbf51464e 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 = {
@@ -13,8 +13,8 @@ const propTypes = {
   intl: PropTypes.shape({
     formatMessage: PropTypes.func.isRequired,
   }).isRequired,
-  rovingIndex: PropTypes.func.isRequired,
   isPublicChat: PropTypes.func.isRequired,
+  roving: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
@@ -32,80 +32,102 @@ const listTransition = {
 };
 
 const intlMessages = defineMessages({
-  usersTitle: {
-    id: 'app.userList.usersTitle',
-    description: 'Title for the Header',
-  },
   messagesTitle: {
     id: 'app.userList.messagesTitle',
     description: 'Title for the messages list',
   },
-  participantsTitle: {
-    id: 'app.userList.participantsTitle',
-    description: 'Title for the Users list',
-  },
-  toggleCompactView: {
-    id: 'app.userList.toggleCompactView.label',
-    description: 'Toggle user list view mode',
-  },
-  ChatLabel: {
-    id: 'app.userList.menu.chat.label',
-    description: 'Save the changes and close the settings menu',
-  },
-  ClearStatusLabel: {
-    id: 'app.userList.menu.clearStatus.label',
-    description: 'Clear the emoji status of this user',
-  },
-  MakePresenterLabel: {
-    id: 'app.userList.menu.makePresenter.label',
-    description: 'Set this user to be the presenter in this meeting',
-  },
-  KickUserLabel: {
-    id: 'app.userList.menu.kickUser.label',
-    description: 'Forcefully remove this user from the meeting',
-  },
-  MuteUserAudioLabel: {
-    id: 'app.userList.menu.muteUserAudio.label',
-    description: 'Forcefully mute this user',
-  },
-  UnmuteUserAudioLabel: {
-    id: 'app.userList.menu.unmuteUserAudio.label',
-    description: 'Forcefully unmute this user',
-  },
-  PromoteUserLabel: {
-    id: 'app.userList.menu.promoteUser.label',
-    description: 'Forcefully promote this viewer to a moderator',
-  },
-  DemoteUserLabel: {
-    id: 'app.userList.menu.demoteUser.label',
-    description: 'Forcefully demote this moderator to a viewer',
-  },
 });
 
 class UserMessages extends Component {
+  constructor() {
+    super();
+
+    this.state = {
+      index: -1,
+    };
+
+    this.openChatRefs = [];
+    this.selectedIndex = -1;
+
+    this.focusOpenChatItem = this.focusOpenChatItem.bind(this);
+    this.changeState = this.changeState.bind(this);
+  }
+
   componentDidMount() {
     if (!this.props.compact) {
       this._msgsList.addEventListener(
         'keydown',
-        event => this.props.rovingIndex(
+        event => this.props.roving(
           event,
-          this._msgsList,
-          this._msgItems,
           this.props.openChats.length,
+          this.changeState,
         ),
       );
     }
   }
 
-  render() {
+  componentDidUpdate(prevProps, prevState) {
+    if (this.state.index === -1) {
+      return;
+    }
+
+    if (this.state.index !== prevState.index) {
+      this.focusOpenChatItem(this.state.index);
+    }
+  }
+
+  getOpenChats() {
     const {
       openChats,
       openChat,
-      intl,
       compact,
       isPublicChat,
     } = this.props;
 
+    let index = -1;
+
+    return openChats.map(chat => (
+      <CSSTransition
+        classNames={listTransition}
+        appear
+        enter
+        exit={false}
+        timeout={0}
+        component="div"
+        className={cx(styles.chatsList)}
+        key={chat.id}
+      >
+        <div ref={(node) => { this.openChatRefs[index += 1] = node; }}>
+          <ChatListItem
+            isPublicChat={isPublicChat}
+            compact={compact}
+            openChat={openChat}
+            chat={chat}
+            tabIndex={-1}
+          />
+        </div>
+      </CSSTransition>
+    ));
+  }
+
+  changeState(newIndex) {
+    this.setState({ index: newIndex });
+  }
+
+  focusOpenChatItem(index) {
+    if (!this.openChatRefs[index]) {
+      return;
+    }
+
+    this.openChatRefs[index].firstChild.focus();
+  }
+
+  render() {
+    const {
+      intl,
+      compact,
+    } = this.props;
+
     return (
       <div className={styles.messages}>
         {
@@ -120,28 +142,9 @@ class UserMessages extends Component {
           className={styles.scrollableList}
           ref={(ref) => { this._msgsList = ref; }}
         >
-          <div ref={(ref) => { this._msgItems = ref; }}>
-            <TransitionGroup>
-              {openChats.map(chat => (
-                <CSSTransition
-                  classNames={listTransition}
-                  appear
-                  enter
-                  exit={false}
-                  timeout={0}
-                  component="div"
-                  className={cx(styles.chatsList)}
-                  key={chat.id}
-                >
-                  <ChatListItem
-                    isPublicChat={isPublicChat}
-                    compact={compact}
-                    openChat={openChat}
-                    chat={chat}
-                    tabIndex={-1}
-                  />
-                </CSSTransition>
-              ))}
+          <div className={styles.list}>
+            <TransitionGroup ref={(ref) => { this._msgItems = ref; }} >
+              { this.getOpenChats() }
             </TransitionGroup>
           </div>
         </div>
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 3f3cc128b073e709889776ac4cff34d88ec2cf5a..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-messages/styles.scss
+++ /dev/null
@@ -1,23 +0,0 @@
-@import "/imports/ui/components/user-list/styles.scss";
-
-.smallTitle {
-  @extend .smallTitle;
-}
-
-.scrollableList {
-  @include scrollbox-vertical($user-list-bg);
-  @extend %customListFocus;
-}
-
-.separator {
-  margin: 1rem auto;
-  width: 2.2rem;
-  border: 0;
-  border-top: 1px solid $color-gray-lighter;
-}
-
-.chatsList {
-  @extend .lists;
-  overflow-x: hidden;
-  flex-shrink: 1;
-}
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx
index 1278d04b69cfbe63a17a65177978e84b6c78db4b..b5b55c34bdd1d9c653f01b1454a5ace3a02da4bc 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx
@@ -23,7 +23,7 @@ const propTypes = {
   getAvailableActions: PropTypes.func.isRequired,
   normalizeEmojiName: PropTypes.func.isRequired,
   isMeetingLocked: PropTypes.func.isRequired,
-  rovingIndex: PropTypes.func.isRequired,
+  roving: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
@@ -48,18 +48,6 @@ const intlMessages = defineMessages({
     id: 'app.userList.usersTitle',
     description: 'Title for the Header',
   },
-  messagesTitle: {
-    id: 'app.userList.messagesTitle',
-    description: 'Title for the messages list',
-  },
-  participantsTitle: {
-    id: 'app.userList.participantsTitle',
-    description: 'Title for the Users list',
-  },
-  toggleCompactView: {
-    id: 'app.userList.toggleCompactView.label',
-    description: 'Toggle user list view mode',
-  },
   ChatLabel: {
     id: 'app.userList.menu.chat.label',
     description: 'Save the changes and close the settings menu',
@@ -98,88 +86,157 @@ class UserParticipants extends Component {
   constructor() {
     super();
 
+    this.state = {
+      index: -1,
+    };
+
+    this.userRefs = [];
+    this.selectedIndex = -1;
+
     this.getScrollContainerRef = this.getScrollContainerRef.bind(this);
+    this.focusUserItem = this.focusUserItem.bind(this);
+    this.changeState = this.changeState.bind(this);
+    this.getUsers = this.getUsers.bind(this);
   }
 
   componentDidMount() {
     if (!this.props.compact) {
       this.refScrollContainer.addEventListener(
         'keydown',
-        event => this.props.rovingIndex(
+        event => this.props.roving(
           event,
-          this.refScrollContainer,
-          this.refScrollItems,
           this.props.users.length,
+          this.changeState,
         ),
       );
     }
   }
 
+  componentDidUpdate(prevProps, prevState) {
+    if (this.state.index === -1) {
+      return;
+    }
+
+    if (this.state.index !== prevState.index) {
+      this.focusUserItem(this.state.index);
+    }
+  }
+
   getScrollContainerRef() {
     return this.refScrollContainer;
   }
 
-  render() {
+  getUsers() {
     const {
-      users,
-      currentUser,
+      compact,
       isBreakoutRoom,
-      intl,
+      currentUser,
       meeting,
       getAvailableActions,
       normalizeEmojiName,
       isMeetingLocked,
-      compact,
-      setEmojiStatus,
+      users,
+      intl,
+      changeRole,
       assignPresenter,
+      setEmojiStatus,
       kickUser,
       toggleVoice,
-      changeRole,
     } = this.props;
 
     const userActions =
-      {
-        openChat: {
-          label: () => intl.formatMessage(intlMessages.ChatLabel),
-          handler: (router, user) => router.push(`/users/chat/${user.id}`),
-          icon: 'chat',
-        },
-        clearStatus: {
-          label: () => intl.formatMessage(intlMessages.ClearStatusLabel),
-          handler: user => setEmojiStatus(user.id, 'none'),
-          icon: 'clear_status',
-        },
-        setPresenter: {
-          label: () => intl.formatMessage(intlMessages.MakePresenterLabel),
-          handler: user => assignPresenter(user.id),
-          icon: 'presentation',
-        },
-        kick: {
-          label: user => intl.formatMessage(intlMessages.KickUserLabel, { 0: user.name }),
-          handler: user => kickUser(user.id),
-          icon: 'circle_close',
-        },
-        mute: {
-          label: () => intl.formatMessage(intlMessages.MuteUserAudioLabel),
-          handler: user => toggleVoice(user.id),
-          icon: 'audio_off',
-        },
-        unmute: {
-          label: () => intl.formatMessage(intlMessages.UnmuteUserAudioLabel),
-          handler: user => toggleVoice(user.id),
-          icon: 'audio_on',
-        },
-        promote: {
-          label: user => intl.formatMessage(intlMessages.PromoteUserLabel, { 0: user.name }),
-          handler: user => changeRole(user.id, 'MODERATOR'),
-          icon: 'promote',
-        },
-        demote: {
-          label: user => intl.formatMessage(intlMessages.DemoteUserLabel, { 0: user.name }),
-          handler: user => changeRole(user.id, 'VIEWER'),
-          icon: 'user',
-        },
-      };
+    {
+      openChat: {
+        label: () => intl.formatMessage(intlMessages.ChatLabel),
+        handler: (router, user) => router.push(`/users/chat/${user.id}`),
+        icon: 'chat',
+      },
+      clearStatus: {
+        label: () => intl.formatMessage(intlMessages.ClearStatusLabel),
+        handler: user => setEmojiStatus(user.id, 'none'),
+        icon: 'clear_status',
+      },
+      setPresenter: {
+        label: () => intl.formatMessage(intlMessages.MakePresenterLabel),
+        handler: user => assignPresenter(user.id),
+        icon: 'presentation',
+      },
+      kick: {
+        label: user => intl.formatMessage(intlMessages.KickUserLabel, { 0: user.name }),
+        handler: user => kickUser(user.id),
+        icon: 'circle_close',
+      },
+      mute: {
+        label: () => intl.formatMessage(intlMessages.MuteUserAudioLabel),
+        handler: user => toggleVoice(user.id),
+        icon: 'audio_off',
+      },
+      unmute: {
+        label: () => intl.formatMessage(intlMessages.UnmuteUserAudioLabel),
+        handler: user => toggleVoice(user.id),
+        icon: 'audio_on',
+      },
+      promote: {
+        label: user => intl.formatMessage(intlMessages.PromoteUserLabel, { 0: user.name }),
+        handler: user => changeRole(user.id, 'MODERATOR'),
+        icon: 'promote',
+      },
+      demote: {
+        label: user => intl.formatMessage(intlMessages.DemoteUserLabel, { 0: user.name }),
+        handler: user => changeRole(user.id, 'VIEWER'),
+        icon: 'user',
+      },
+    };
+
+    let index = -1;
+
+    return users.map(user => (
+      <CSSTransition
+        classNames={listTransition}
+        appear
+        enter
+        exit
+        timeout={0}
+        component="div"
+        className={cx(styles.participantsList)}
+        key={user.id}
+      >
+        <div ref={(node) => { this.userRefs[index += 1] = node; }}>
+          <UserListItem
+            compact={compact}
+            isBreakoutRoom={isBreakoutRoom}
+            user={user}
+            currentUser={currentUser}
+            userActions={userActions}
+            meeting={meeting}
+            getAvailableActions={getAvailableActions}
+            normalizeEmojiName={normalizeEmojiName}
+            isMeetingLocked={isMeetingLocked}
+            getScrollContainerRef={this.getScrollContainerRef}
+          />
+        </div>
+      </CSSTransition>
+    ));
+  }
+
+  focusUserItem(index) {
+    if (!this.userRefs[index]) {
+      return;
+    }
+
+    this.userRefs[index].firstChild.focus();
+  }
+
+  changeState(newIndex) {
+    this.setState({ index: newIndex });
+  }
+
+  render() {
+    const {
+      users,
+      intl,
+      compact,
+    } = this.props;
 
     return (
       <div className={styles.participants}>
@@ -196,33 +253,9 @@ class UserParticipants extends Component {
           tabIndex={0}
           ref={(ref) => { this.refScrollContainer = ref; }}
         >
-          <div ref={(ref) => { this.refScrollItems = ref; }}>
-            <TransitionGroup>
-              { users.map(user => (
-                <CSSTransition
-                  classNames={listTransition}
-                  appear
-                  enter
-                  exit
-                  timeout={0}
-                  component="div"
-                  className={cx(styles.participantsList)}
-                  key={user.id}
-                >
-                  <UserListItem
-                    compact={compact}
-                    isBreakoutRoom={isBreakoutRoom}
-                    user={user}
-                    currentUser={currentUser}
-                    userActions={userActions}
-                    meeting={meeting}
-                    getAvailableActions={getAvailableActions}
-                    normalizeEmojiName={normalizeEmojiName}
-                    isMeetingLocked={isMeetingLocked}
-                    getScrollContainerRef={this.getScrollContainerRef}
-                  />
-                </CSSTransition>
-              ))}
+          <div className={styles.list}>
+            <TransitionGroup ref={(ref) => { this.refScrollItems = ref; }}>
+              { this.getUsers() }
             </TransitionGroup>
           </div>
         </div>
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx
index 4fbb487363cec7295a8169cfb5c9a99f8b61c90e..fe70a3f4a39eef557baaacc5bc2a803c9477af27 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx
@@ -25,7 +25,7 @@ const propTypes = {
   }).isRequired,
   userActions: PropTypes.shape({}).isRequired,
   router: PropTypes.shape({}).isRequired,
-  isBreakoutRoom: PropTypes.bool.isRequired,
+  isBreakoutRoom: PropTypes.bool,
   getAvailableActions: PropTypes.func.isRequired,
   meeting: PropTypes.shape({}).isRequired,
   isMeetingLocked: PropTypes.func.isRequired,
@@ -34,12 +34,10 @@ const propTypes = {
 };
 
 const defaultProps = {
-  shouldShowActions: false,
   isBreakoutRoom: false,
 };
 
 class UserListItem extends Component {
-
   static createAction(action, ...options) {
     return (
       <UserAction
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/component.jsx
index 9af1a7808be463022222e4aa582b5e3106c32d82..17f4b7ae3deb70aaca89c87404d19c99f6896437 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/component.jsx
@@ -57,7 +57,6 @@ const propTypes = {
 
 
 class UserListContent extends Component {
-
   /**
    * Return true if the content fit on the screen, false otherwise.
    *
@@ -140,8 +139,10 @@ class UserListContent extends Component {
       };
 
       const isDropdownVisible =
-        UserListContent.checkIfDropdownIsVisible(dropdownContent.offsetTop,
-          dropdownContent.offsetHeight);
+        UserListContent.checkIfDropdownIsVisible(
+          dropdownContent.offsetTop,
+          dropdownContent.offsetHeight,
+        );
 
       if (!isDropdownVisible) {
         const offsetPageTop =
@@ -201,13 +202,15 @@ class UserListContent extends Component {
       ? intl.formatMessage(messages.presenter)
       : '';
 
-    const userAriaLabel = intl.formatMessage(messages.userAriaLabel,
+    const userAriaLabel = intl.formatMessage(
+      messages.userAriaLabel,
       {
         0: user.name,
         1: presenter,
         2: you,
         3: user.emoji.status,
-      });
+      },
+    );
 
     const contents = (
       <div
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/styles.scss
index f68682e17ceafc85b9a5e6a18a59869dc90699f8..79e347ff65801674530963c4f0b64266c2a33da3 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/styles.scss
@@ -1,4 +1,5 @@
 @import "/imports/ui/components/user-list/styles.scss";
+@import "/imports/ui/stylesheets/variables/_all";
 
 /* Animations
  * ==========
@@ -62,18 +63,15 @@
 
 .active {
   background-color: $list-item-bg-hover;
+  box-shadow: inset 0 0 0 $border-size $item-focus-border, inset 1px 0 0 1px $item-focus-border;
   outline: none;
 }
 
 .userListItem {
-  @extend %customUserItemFocus;
+
   @extend %list-item;
   flex-flow: column;
   flex-shrink: 0;
-
-  &:last-child {
-    margin-bottom: 0.5rem;
-  }
 }
 
 .userAvatar {
@@ -84,6 +82,10 @@
   flex-grow: 0;
   display: flex;
   flex-flow: row;
+
+  padding-top: $lg-padding-y;
+  padding-bottom: $lg-padding-y;
+  padding-left: $lg-padding-y;
 }
 
 .dropdown {
@@ -97,5 +99,3 @@
   max-width: 100%;
   overflow: visible;
 }
-
-
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-header/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-header/component.jsx
deleted file mode 100644
index c78e204fa1d831fa1c90b537749e4d40fe89a113..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-header/component.jsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import React from 'react';
-import { defineMessages } from 'react-intl';
-import PropTypes from 'prop-types';
-import styles from './styles';
-
-const intlMessages = defineMessages({
-  participantsTitle: {
-    id: 'app.userList.participantsTitle',
-    description: 'Title for the Users list',
-  },
-});
-
-const propTypes = {
-  compact: PropTypes.bool,
-  intl: PropTypes.shape({ formatMessage: PropTypes.func.isRequired }).isRequired,
-};
-
-const defaultProps = {
-  compact: false,
-};
-
-const UserListHeader = props => (
-  <div className={styles.header}>
-    {
-      !props.compact ?
-        <div className={styles.headerTitle} role="banner">
-          {props.intl.formatMessage(intlMessages.participantsTitle)}
-        </div> : null
-    }
-    {/* <Button
-        label={intl.formatMessage(intlMessages.toggleCompactView)}
-        hideLabel
-        icon={!this.state.compact ? 'left_arrow' : 'right_arrow'}
-        className={styles.btnToggle}
-        onClick={this.handleToggleCompactView}
-      /> */}
-  </div>
-);
-
-UserListHeader.propTypes = propTypes;
-UserListHeader.defaultProps = defaultProps;
-
-export default UserListHeader;
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-header/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/user-list-header/styles.scss
deleted file mode 100644
index 8a9410dffb3d491f88cb1ef49814b3c64a8df9bb..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-header/styles.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-@import "/imports/ui/components/user-list/styles.scss";
-
-.header {
-  @extend %flex-column;
-  justify-content: left;
-  flex-grow: 0;
-  display: flex;
-  flex-direction: row;
-  padding: 0 $md-padding-x;
-  margin: $md-padding-x 0;
-}
-
-.headerTitle {
-  flex: 0;
-  font-size: 1rem;
-  font-weight: 600;
-  color: $color-heading;
-}
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..1d36e6d8dbe62cde6324c89b189b835d674ca294 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,20 @@ 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;
+    const Android = navigator.userAgent.toLowerCase().indexOf('android') > -1;
+
+    // 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 || (Android && unsupportedFirefox)) { return; }
+
     if (this.props.isActive && this.props.annotation.status !== DRAW_END) {
       this.handleFocus();
     }
@@ -170,6 +184,7 @@ export default class TextDrawComponent extends Component {
             onChange={this.onChangeHandler}
             onBlur={this.handleOnBlur}
             style={styles}
+            spellCheck="false"
           />
         </foreignObject>
       </g>
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/components/whiteboard/whiteboard-toolbar/toolbar-submenu/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu/component.jsx
index 645a586a1586381e0362de9f66788d37d237c2c5..75351247aefdd9afc3c86a474cb3494e75c3ffec 100644
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/toolbar-submenu/component.jsx
@@ -80,7 +80,7 @@ export default class ToolbarSubmenu extends Component {
         {objectsToRender ? objectsToRender.map(obj =>
           (
             <ToolbarSubmenuItem
-              label={label}
+              label={obj.value}
               icon={!customIcon ? obj.icon : null}
               customIcon={customIcon ? ToolbarSubmenu.getCustomIcon(type, obj) : null}
               onItemClick={this.onItemClick}
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/ui/services/settings/index.js b/bigbluebutton-html5/imports/ui/services/settings/index.js
index 233870a194c52cbac07eaa67c02c6c1c57a3ab20..97d8940be2471b61471b4835a5d20c0101bf7f75 100644
--- a/bigbluebutton-html5/imports/ui/services/settings/index.js
+++ b/bigbluebutton-html5/imports/ui/services/settings/index.js
@@ -32,7 +32,7 @@ class Settings {
     });
 
     // Sets default locale to browser locale
-    defaultValues.application.locale = navigator.languages[0] ||
+    defaultValues.application.locale = navigator.languages ? navigator.languages[0] : false ||
                                        navigator.language ||
                                        defaultValues.application.locale;
 
diff --git a/bigbluebutton-html5/imports/ui/stylesheets/mixins/focus.scss b/bigbluebutton-html5/imports/ui/stylesheets/mixins/focus.scss
new file mode 100644
index 0000000000000000000000000000000000000000..6c591be1a2e9e159cd3b3cd23892bf8830335387
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/stylesheets/mixins/focus.scss
@@ -0,0 +1,18 @@
+@import "/imports/ui/stylesheets/variables/palette";
+@import "/imports/ui/stylesheets/variables/general";
+
+@mixin elementFocus($color: $color-primary){
+  &:focus {
+    outline: none;
+    box-shadow: inset 0 0 0 $border-size-large $color;
+    border-radius: $border-size;
+  }
+}
+
+@mixin inputFocus($shadow, $color: $color-primary) {
+  &:focus {
+    outline: none;
+    border-radius: $border-size;
+    box-shadow: 0 0 0 $border-size $shadow, inset 0 0 0 1px $color;
+  }
+} 
diff --git a/bigbluebutton-html5/imports/ui/stylesheets/variables/_all.scss b/bigbluebutton-html5/imports/ui/stylesheets/variables/_all.scss
index 8c8f114c496fec3b66ead295d923e4e5503280f3..7813d0ab943174acc90567ecb4dbbdb6182bf62e 100644
--- a/bigbluebutton-html5/imports/ui/stylesheets/variables/_all.scss
+++ b/bigbluebutton-html5/imports/ui/stylesheets/variables/_all.scss
@@ -2,4 +2,3 @@
 @import "./breakpoints";
 @import "./palette";
 @import "./typography";
-@import "./focus";
diff --git a/bigbluebutton-html5/imports/ui/stylesheets/variables/focus.scss b/bigbluebutton-html5/imports/ui/stylesheets/variables/focus.scss
deleted file mode 100644
index f1a65e09f8c8427235d4acb626b05010403c8a9f..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/stylesheets/variables/focus.scss
+++ /dev/null
@@ -1,104 +0,0 @@
-$focus-border-size: 2px;
-$focus-border-offset: -4px;
-$color-primary: #0F70D7 !default;
-$color-primary-input-shadow: lighten($color-primary, 35%);
-$user-list-bg: #F3F6F9;
-$color-white: #FFF;
-$list-item-bg-hover: darken($user-list-bg, 7%);
-
-%customListFocus {
-  border: $focus-border-size solid $user-list-bg;
-  border-radius: $focus-border-size;
-
-  &:focus {
-    outline: none;
-    border: $focus-border-size solid $color-primary;
-  }
-}
-
-%customUserItemFocus,
-%customChatItemFocus {
-  border: $focus-border-size solid $user-list-bg;
-  border-radius: $focus-border-size;
-
-  &:hover {
-    background-color: $list-item-bg-hover;
-  }
-
-  &,
-  &:active {
-    &:focus {
-      outline: none;
-      border: $focus-border-size solid $user-list-bg;
-      border-left: $focus-border-size solid $color-primary;
-      padding-left: $focus-border-size;
-      border-radius: $focus-border-size;
-    }
-  }
-}
-
-%customListItemFocus {
-  position: relative;
-  display: inline-block;
-  border-right: $focus-border-size solid $color-white;
-  border-bottom: 1px solid $color-white; 
-
-  &,
-  &:active {
-    &:focus {
-      outline: none;
-      border-right: $focus-border-size solid $color-primary;
-      border-bottom: 1px solid $color-primary;     
-    }
-  }
-}
-
-%customInputFocus {
-  &,
-  &:active {
-    &:focus {
-      outline: none;
-      border: 1px solid $color-primary;
-      border-radius: $focus-border-size;
-      box-shadow: 0 0 0 2px $color-primary-input-shadow;
-    }
-  }
-}
-
-%customSelectFocus,
-%customLinkFocus {
-  border: $focus-border-size solid $color-white;
-
-  &,
-  &:active {
-    &:focus {
-      outline: none;
-      border: $focus-border-size solid $color-primary;
-      border-radius: $focus-border-size; 
-    }
-  }
-}
-
-%btnFocus {
-  border: none;
-  position: relative;
-  overflow: visible;
-  display: inline-block;
-  
-  &,
-  &:active {
-    &:focus {
-      outline: none;
-
-      span:first-of-type::before {
-        position: absolute;
-        content: '';
-        top: $focus-border-offset;
-        left: $focus-border-offset;
-        right: $focus-border-offset;
-        bottom: $focus-border-offset;
-        border: $focus-border-size solid $color-primary;
-      }
-    }
-  }
-}
diff --git a/bigbluebutton-html5/imports/ui/stylesheets/variables/general.scss b/bigbluebutton-html5/imports/ui/stylesheets/variables/general.scss
index e6171d2f0939a5bbc9bf131a096059c236c09fd8..88f7cde2563f68919749aea436ad6d65ce9825cb 100644
--- a/bigbluebutton-html5/imports/ui/stylesheets/variables/general.scss
+++ b/bigbluebutton-html5/imports/ui/stylesheets/variables/general.scss
@@ -1,15 +1,16 @@
 
 $border-size: 2px;
+$border-size-large: 3px;
 $border-radius: .2rem;
 
 $sm-padding-x: .75rem;
-$sm-padding-y: .25rem;
+$sm-padding-y: .3rem;
 
 $md-padding-x: 1rem;
-$md-padding-y: .375rem;
+$md-padding-y: .45rem;
 
 $lg-padding-x: 1.25rem;
-$lg-padding-y: .5rem;
+$lg-padding-y: 0.6rem;
 
 $jumbo-padding-x: 3.025rem;
-$jumbo-padding-y: 1.25rem;
+$jumbo-padding-y: 1.5rem;
diff --git a/bigbluebutton-html5/imports/ui/stylesheets/variables/palette.scss b/bigbluebutton-html5/imports/ui/stylesheets/variables/palette.scss
index 7943da981f2eadd91441f0272d103bd071b020cf..f7ac7ebc4bf22dcc42df77497d9aa9c25a00f22f 100644
--- a/bigbluebutton-html5/imports/ui/stylesheets/variables/palette.scss
+++ b/bigbluebutton-html5/imports/ui/stylesheets/variables/palette.scss
@@ -1,4 +1,5 @@
 $color-white: #FFF !default;
+$color-off-white: #F3F6F9;
 
 $color-gray: #4E5A66 !default;
 $color-gray-dark: #06172A !default;
@@ -19,3 +20,7 @@ $color-link: $color-primary !default;
 $color-link-hover: darken($color-link, 15%) !default;
 
 $color-gray-label: $color-gray !default;
+
+$color-blue-light: lighten($color-primary, 35%);
+$color-blue-lighter: #92BCEA;
+$color-blue-lightest: #E4ECF2;
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/package-lock.json b/bigbluebutton-html5/package-lock.json
index 759240950ac7240db1e39ab69d5984252504b7f4..f4798f2b93be27ea001c29e45916bfd5f038c07d 100644
--- a/bigbluebutton-html5/package-lock.json
+++ b/bigbluebutton-html5/package-lock.json
@@ -215,10 +215,6 @@
       "integrity": "sha1-lfE2KbEsOlGl0hWr3OKqnzL4B3M=",
       "dev": true
     },
-    "attr-accept": {
-      "version": "https://registry.npmjs.org/attr-accept/-/attr-accept-1.1.0.tgz",
-      "integrity": "sha1-tc01In8WOTWo8d4Q7T66FpQfa+Y="
-    },
     "autoprefixer": {
       "version": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.1.6.tgz",
       "integrity": "sha1-+5MwOfdK90qD5xIlznjZ/Vi6hNc=",
@@ -1282,7 +1278,7 @@
     },
     "fibers": {
       "version": "https://registry.npmjs.org/fibers/-/fibers-2.0.0.tgz",
-      "integrity": "sha1-8m0Krx+ZmV++HLPzQO+sCL2p3Es=",
+      "integrity": "sha512-sLxo4rZVk7xLgAjb/6zEzHJfSALx6u6coN1z61XCOF7i6CyTdJawF4+RdpjCSeS8AP66eR2InScbYAz9RAVOgA==",
       "dev": true
     },
     "figures": {
@@ -1427,7 +1423,7 @@
     },
     "glob": {
       "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
-      "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+      "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
       "requires": {
         "fs.realpath": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
         "inflight": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -1593,7 +1589,7 @@
     },
     "humanize-duration": {
       "version": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.10.1.tgz",
-      "integrity": "sha1-ZbVQwKoJUVbst8NA20TuC99xr0s=",
+      "integrity": "sha512-FHD+u5OKj8TSsSdMHJxSCC78N5Rt4ecil6sWvI+xPbUKhxvHmkKo/V8imbR1m2dXueZYLIl7PcSYX9i/oEiOIA==",
       "dev": true
     },
     "husky": {
@@ -1629,7 +1625,7 @@
     },
     "immutability-helper": {
       "version": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-2.4.0.tgz",
-      "integrity": "sha1-ANQh4pV8F/DweBR18F/9g35zRY0=",
+      "integrity": "sha512-rW/L/56ZMo9NStMK85kFrUFFGy4NeJbCdhfrDHIZrFfxYtuwuxD+dT3mWMcdmrNO61hllc60AeGglCRhfZ1dZw==",
       "requires": {
         "invariant": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz"
       }
@@ -3503,6 +3499,12 @@
       "integrity": "sha1-KYuJ34uTsCIdv0Ia0rGx6iP8Z3c=",
       "dev": true
     },
+    "popper.js": {
+      "version": "1.12.9",
+      "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.12.9.tgz",
+      "integrity": "sha1-DfvC3/lsRRuzMu3Pz6r1ZtMx1bM=",
+      "dev": true
+    },
     "postcss": {
       "version": "https://registry.npmjs.org/postcss/-/postcss-6.0.13.tgz",
       "integrity": "sha1-ueyrTuAMids+yTEUW9lZC78/El8=",
@@ -3745,14 +3747,6 @@
         "prop-types": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz"
       }
     },
-    "react-dropzone": {
-      "version": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-4.2.1.tgz",
-      "integrity": "sha1-aV6AvQsGXxGB5p8tD20dXMcmZMk=",
-      "requires": {
-        "attr-accept": "https://registry.npmjs.org/attr-accept/-/attr-accept-1.1.0.tgz",
-        "prop-types": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz"
-      }
-    },
     "react-intl": {
       "version": "https://registry.npmjs.org/react-intl/-/react-intl-2.4.0.tgz",
       "integrity": "sha1-ZsFNyd+ac7L7v71gIXJugKYT6xU=",
@@ -3784,20 +3778,13 @@
         "warning": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz"
       }
     },
-    "react-tabs": {
-      "version": "https://registry.npmjs.org/react-tabs/-/react-tabs-2.1.0.tgz",
-      "integrity": "sha1-uhhKUZ4KCAPPeQoesZvE/bpf0Oo=",
-      "requires": {
-        "classnames": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz",
-        "prop-types": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz"
-      }
-    },
-    "react-toastify": {
-      "version": "https://registry.npmjs.org/react-toastify/-/react-toastify-2.1.6.tgz",
-      "integrity": "sha1-Gkh/rSekjZ6u9FaDXpVevnmxp5A=",
+    "react-tippy": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/react-tippy/-/react-tippy-1.2.2.tgz",
+      "integrity": "sha512-xqmymAhKub1JGtLJ+HncUauBpwJjHAp6EkKBLeGtuhneaGQ3GnRp5aEd/YRNc4NmIb6o1lbf/Z6R9G3/VjnjYA==",
+      "dev": true,
       "requires": {
-        "prop-types": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz",
-        "react-transition-group": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.2.1.tgz"
+        "popper.js": "1.12.9"
       }
     },
     "react-toggle": {
@@ -4396,7 +4383,7 @@
     },
     "tiny-emitter": {
       "version": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.2.tgz",
-      "integrity": "sha1-gtJ0aKylrejl/R5tIrV91D69+3w="
+      "integrity": "sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow=="
     },
     "tinycolor2": {
       "version": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz",
diff --git a/bigbluebutton-html5/package.json b/bigbluebutton-html5/package.json
index 031ba58a0fa4bc8f9c2e1404d918969a1f8d41a5..705ed0c82e46cf72afd3687229b0ae23351a8b9d 100755
--- a/bigbluebutton-html5/package.json
+++ b/bigbluebutton-html5/package.json
@@ -41,11 +41,12 @@
     "react-modal": "~3.0.4",
     "react-router": "~3.0.2",
     "react-tabs": "~2.1.0",
-    "react-toggle": "~4.0.2",
     "react-toastify": "~2.1.2",
+    "react-toggle": "~4.0.2",
     "react-transition-group": "~2.2.1",
     "redis": "~2.8.0",
     "string-hash": "~1.1.3",
+    "tippy.js": "^2.0.2",
     "winston": "~2.4.0",
     "xml2js": "~0.4.19"
   },
diff --git a/bigbluebutton-html5/private/config/public/acl.yaml b/bigbluebutton-html5/private/config/public/acl.yaml
index 3feda493b5f9ad7aa78b9dbe247d6cc3cab7fbc1..2bd75779a3509a0feb5341ddafa64a8d15311015 100644
--- a/bigbluebutton-html5/private/config/public/acl.yaml
+++ b/bigbluebutton-html5/private/config/public/acl.yaml
@@ -31,6 +31,7 @@ acl:
       - 'toggleVoice'
       - 'clearPublicChatHistory'
       - 'changeRole'
+      - 'ejectUserFromVoice'
   presenter:
     methods:
       - 'assignPresenter'
diff --git a/bigbluebutton-html5/private/config/public/app.yaml b/bigbluebutton-html5/private/config/public/app.yaml
index 31a43a21b31284e8afeaeac824901d84a66d4087..7dffde82262185e1195ca62804eba5c573807ac7 100644
--- a/bigbluebutton-html5/private/config/public/app.yaml
+++ b/bigbluebutton-html5/private/config/public/app.yaml
@@ -54,3 +54,4 @@ app:
   # The initial client version has limited moderator capabilities
   # The following flag disables moderator-only features
   allowHTML5Moderator: true
+  allowModeratorToUnmuteAudio: true
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/config/server/redis.yaml b/bigbluebutton-html5/private/config/server/redis.yaml
index cefc6d8ebad4eb58ad0a8a9be4bf2f9f9b1ab2eb..9085b05efe3705b3d6aba68772c991f022b0b1c2 100644
--- a/bigbluebutton-html5/private/config/server/redis.yaml
+++ b/bigbluebutton-html5/private/config/server/redis.yaml
@@ -12,3 +12,4 @@ redis:
     - 'from-akka-apps-wb-redis-channel'
   ignored:
     - 'CheckAlivePongSysMsg'
+    - 'DoLatencyTracerMsg'
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json
index fef395731f67acece496fa4aa8d0362f17efa635..728ec78ae34f2731b4b648c3b788838be27be730 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",
@@ -49,6 +52,7 @@
     "app.presentation.presentationToolbar.fitScreenDesc": "Display the whole slide",
     "app.presentation.presentationToolbar.zoomLabel": "Zoom",
     "app.presentation.presentationToolbar.zoomDesc": "Change the zoom level of the presentation",
+    "app.presentation.presentationToolbar.goToSlide":"Slide {0}",
     "app.presentationUploder.title": "Presentation",
     "app.presentationUploder.message": "As a presenter in BigBlueButton, you have the ability of uploading any office document or PDF file. We recommend for the best results, to please upload a PDF file.",
     "app.presentationUploder.confirmLabel": "Start",
@@ -58,13 +62,17 @@
     "app.presentationUploder.dropzoneLabel": "Drag files here to upload",
     "app.presentationUploder.browseFilesLabel": "or browse for files",
     "app.presentationUploder.fileToUpload": "To be uploaded...",
+    "app.presentationUploder.currentBadge": "Current",
+    "app.presentationUploder.genericError": "Ops, something went wrong",
     "app.presentationUploder.upload.progress": "Uploading ({progress}%)",
-    "app.presentationUploder.upload.413": "File is too large.",
+    "app.presentationUploder.upload.413": "File is too large",
     "app.presentationUploder.conversion.conversionProcessingSlides": "Processing page {current} of {total}",
     "app.presentationUploder.conversion.genericConversionStatus": "Converting file...",
     "app.presentationUploder.conversion.generatingThumbnail": "Generating thumbnails...",
+    "app.presentationUploder.conversion.generatedSlides": "Slides generated...",
     "app.presentationUploder.conversion.generatingSvg": "Generating SVG images...",
-    "presentationUploder.conversion.generatedSlides": "Slides generated...",
+    "app.presentationUploder.conversion.pageCountExceeded": "Ops, the page count exceeded the limit",
+    "app.presentationUploder.conversion.timeout": "Ops, the conversion is taking too long",
     "app.polling.pollingTitle": "Polling Options",
     "app.failedMessage": "Apologies, trouble connecting to the server.",
     "app.connectingMessage": "Connecting...",
@@ -173,18 +181,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",
@@ -224,6 +232,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",
@@ -233,7 +243,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.video.joinVideo": "Share Webcam",
@@ -249,6 +259,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",
@@ -258,6 +270,8 @@
     "app.error.403": "Forbidden",
     "app.error.leaveLabel": "Log in again",
     "app.guest.waiting": "Waiting for approval to join",
+    "app.toast.chat.singular":"you have {0} new message in {1}",
+    "app.toast.chat.plural":"you have {0} new messages in {1}",
     "app.notification.recordingStart": "This session is now being recorded",
     "app.notification.recordingStop": "This session is not being recorded anymore",
     "app.video.joinVideo": "Share webcam",
diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
index 9b988bf241b00a5f6c54264d502d68f1adc7e7f6..c2356afc6128fde6f1f58f82b9a0fdf0c511e148 100755
--- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
+++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
@@ -164,10 +164,20 @@ bigbluebutton.web.serverURL=http://192.168.246.131
 # 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 default avatar image to display if nothing is passed on the JOIN API (avatarURL)
 # call. This avatar is displayed if the user isn't sharing the webcam and
diff --git a/bigbluebutton-web/grails-app/conf/spring/resources.xml b/bigbluebutton-web/grails-app/conf/spring/resources.xml
index 4912738ec066e36a98d5888b53a883e892efaa03..c8cc4216485de7f7439c58106eda8071d03c6d10 100755
--- a/bigbluebutton-web/grails-app/conf/spring/resources.xml
+++ b/bigbluebutton-web/grails-app/conf/spring/resources.xml
@@ -122,6 +122,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
         <property name="defaultServerUrl" value="${bigbluebutton.web.serverURL}"/>
         <property name="defaultNumDigitsForTelVoice" value="${defaultNumDigitsForTelVoice}"/>
         <property name="defaultClientUrl" value="${defaultClientUrl}"/>
+        <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 1b66c241f24f221e8e522849b4259e205772a01a..5be1a10c1652de6fc9db085138f679fc33e2f94f 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
@@ -162,10 +162,6 @@ class ApiController {
 
     Meeting newMeeting = paramsProcessorUtil.processCreateParams(params);
 
-    if (! StringUtils.isEmpty(params.moderatorOnlyMessage)) {
-      newMeeting.setModeratorOnlyMessage(params.moderatorOnlyMessage);
-    }
-
     if (meetingService.createMeeting(newMeeting)) {
       // See if the request came with pre-uploading of presentation.
       uploadDocuments(newMeeting);
@@ -263,6 +259,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);
@@ -504,6 +505,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);
@@ -512,9 +534,6 @@ class ApiController {
       }
     }
 
-    if(!StringUtils.isEmpty(params.clientURL)){
-      clientURL = params.clientURL;
-    }
 
     if (redirectClient){
       String destUrl = clientURL + "?sessionToken=" + sessionToken
@@ -1358,6 +1377,9 @@ class ApiController {
               welcome = us.welcome
               if (! StringUtils.isEmpty(meeting.moderatorOnlyMessage))
                 modOnlyMessage = meeting.moderatorOnlyMessage
+
+              customLogoURL = meeting.getCustomLogoURL()
+              customCopyright = meeting.getCustomCopyright()
               logoutUrl = us.logoutUrl
               defaultLayout = us.defaultLayout
               avatarURL = us.avatarURL
diff --git a/bigbluebutton-web/web-app/WEB-INF/freemarker/get-meeting-info.ftlx b/bigbluebutton-web/web-app/WEB-INF/freemarker/get-meeting-info.ftlx
index c3ba00308d32fc9b577e99bf4118c5d8b663c058..d7aebe3e2f70209c566c58a280ed6a1fc580eb5a 100755
--- a/bigbluebutton-web/web-app/WEB-INF/freemarker/get-meeting-info.ftlx
+++ b/bigbluebutton-web/web-app/WEB-INF/freemarker/get-meeting-info.ftlx
@@ -27,7 +27,7 @@
   <attendees>
   <#list meeting.getUsers() as att>
     <attendee>
-        <userID>${att.getInternalUserId()}</userID>
+        <userID>${att.getExternalUserId()}</userID>
         <fullName>${att.getFullname()?html}</fullName>
         <role>${att.getRole()}</role>
         <isPresenter>${att.isPresenter()?c}</isPresenter>
diff --git a/bigbluebutton-web/web-app/WEB-INF/freemarker/get-meetings.ftlx b/bigbluebutton-web/web-app/WEB-INF/freemarker/get-meetings.ftlx
index bcc231f5ff244d561d4f8e00951ec115f3dcbb30..a43bfaea9c23ac5679bfb2e773432cb0db2e8ace 100755
--- a/bigbluebutton-web/web-app/WEB-INF/freemarker/get-meetings.ftlx
+++ b/bigbluebutton-web/web-app/WEB-INF/freemarker/get-meetings.ftlx
@@ -32,7 +32,7 @@
         <attendees>
         <#list meetingDetail.meeting.getUsers() as att>
           <attendee>
-              <userID>${att.getInternalUserId()}</userID>
+              <userID>${att.getExternalUserId()}</userID>
               <fullName>${att.getFullname()?html}</fullName>
               <role>${att.getRole()}</role>
               <isPresenter>${att.isPresenter()?c}</isPresenter>
diff --git a/record-and-playback/core/Gemfile b/record-and-playback/core/Gemfile
index 13924f205f88cc413081a3dcd9c1e2c616c5d2b0..e9c9431338da9807d27ddddc4edd4e63810a2b32 100644
--- a/record-and-playback/core/Gemfile
+++ b/record-and-playback/core/Gemfile
@@ -21,11 +21,7 @@ source "http://rubygems.org"
 
 gem "redis"
 gem "nokogiri"
-gem "resque"
-gem "mime-types", "2.99.2"
-gem "streamio-ffmpeg", "2.1.0"
 gem "rubyzip"
-gem "curb"
 gem "builder"
 gem "trollop"
 gem "open4"
diff --git a/record-and-playback/core/Gemfile.lock b/record-and-playback/core/Gemfile.lock
index 6a507fcf74ac4f17d0501f45e16de365025f46b0..6c0b205d28bbfe4f4f72db131fc7e5d32331c866 100644
--- a/record-and-playback/core/Gemfile.lock
+++ b/record-and-playback/core/Gemfile.lock
@@ -2,43 +2,15 @@ GEM
   remote: http://rubygems.org/
   specs:
     absolute_time (1.0.0)
-    addressable (2.4.0)
-    builder (3.2.2)
-    curb (0.9.3)
-    fastimage (2.0.0)
-      addressable (~> 2)
-    mime-types (2.99.2)
-    mini_portile2 (2.1.0)
-    mono_logger (1.1.0)
-    multi_json (1.12.1)
-    nokogiri (1.6.8)
-      mini_portile2 (~> 2.1.0)
-      pkg-config (~> 1.1.7)
+    builder (3.2.3)
+    fastimage (2.1.0)
+    mini_portile2 (2.3.0)
+    nokogiri (1.8.1)
+      mini_portile2 (~> 2.3.0)
     open4 (1.3.4)
-    pkg-config (1.1.7)
-    rack (1.6.4)
-    rack-protection (1.5.3)
-      rack
-    redis (3.3.1)
-    redis-namespace (1.5.2)
-      redis (~> 3.0, >= 3.0.4)
-    resque (1.26.0)
-      mono_logger (~> 1.0)
-      multi_json (~> 1.0)
-      redis-namespace (~> 1.3)
-      sinatra (>= 0.9.2)
-      vegas (~> 0.1.2)
-    rubyzip (1.2.0)
-    sinatra (1.4.7)
-      rack (~> 1.5)
-      rack-protection (~> 1.4)
-      tilt (>= 1.3, < 3)
-    streamio-ffmpeg (2.1.0)
-      multi_json (~> 1.8)
-    tilt (2.0.5)
+    redis (4.0.1)
+    rubyzip (1.2.1)
     trollop (2.1.2)
-    vegas (0.1.11)
-      rack (>= 1.0.0)
 
 PLATFORMS
   ruby
@@ -46,17 +18,12 @@ PLATFORMS
 DEPENDENCIES
   absolute_time
   builder
-  curb
   fastimage
-  mime-types (= 2.99.2)
   nokogiri
   open4
   redis
-  resque
   rubyzip
-  streamio-ffmpeg (= 2.1.0)
   trollop
 
 BUNDLED WITH
-   1.12.5
-
+   1.13.7