diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/GuestsApp.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/GuestsApp.scala
index 823e999df6479933b7012feb2995815847eb55c1..9c97de077ff75ffaa9099f6932114bf8c52e8a53 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/GuestsApp.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/GuestsApp.scala
@@ -7,6 +7,7 @@ trait GuestsApp extends GetGuestsWaitingApprovalReqMsgHdlr
   with GuestsWaitingApprovedMsgHdlr
   with GuestWaitingLeftMsgHdlr
   with SetGuestPolicyMsgHdlr
+  with SetGuestLobbyMessageMsgHdlr
   with GetGuestPolicyReqMsgHdlr {
 
   this: MeetingActor =>
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/GuestsWaiting.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/GuestsWaiting.scala
index 88339acf05d697f4a8df22fbb3944a0909e0a430..67eedfcb48e3ad4f13143ac85d63ef833273b037 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/GuestsWaiting.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/GuestsWaiting.scala
@@ -24,6 +24,9 @@ object GuestsWaiting {
     guests.setGuestPolicy(policy)
   }
 
+  def setGuestLobbyMessage(guests: GuestsWaiting, message: String): Unit = {
+    guests.setGuestLobbyMessage(message)
+  }
 }
 
 class GuestsWaiting {
@@ -31,6 +34,8 @@ class GuestsWaiting {
 
   private var guestPolicy = GuestPolicy(GuestPolicyType.ALWAYS_ACCEPT, SystemUser.ID)
 
+  private var guestLobbyMessage = ""
+
   private def toVector: Vector[GuestWaiting] = guests.values.toVector
 
   private def save(user: GuestWaiting): GuestWaiting = {
@@ -49,6 +54,8 @@ class GuestsWaiting {
 
   def getGuestPolicy(): GuestPolicy = guestPolicy
   def setGuestPolicy(policy: GuestPolicy) = guestPolicy = policy
+
+  def setGuestLobbyMessage(message: String) = guestLobbyMessage = message
 }
 
 case class GuestWaiting(intId: String, name: String, role: String, guest: Boolean, avatar: String, authenticated: Boolean, registeredOn: Long)
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 497e168bb55f0b5361503b0ca6ca23b1b02dae3f..fcb3dcf9251aa78d8738bec997a0ce7eb68d6504 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
@@ -95,6 +95,8 @@ class ReceivedJsonMsgHandlerActor(
         routeGenericMsg[SetGuestPolicyCmdMsg](envelope, jsonNode)
       case GetGuestPolicyReqMsg.NAME =>
         routeGenericMsg[GetGuestPolicyReqMsg](envelope, jsonNode)
+      case SetGuestLobbyMessageCmdMsg.NAME =>
+        routeGenericMsg[SetGuestLobbyMessageCmdMsg](envelope, jsonNode)
 
       // Users
       case GetUsersMeetingReqMsg.NAME =>
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala
index 98172c3dae184bd36ba881a241eecbf410ba1998..066935415b2bd03af600cd1137f9b8c09743181d 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
@@ -443,6 +443,7 @@ class MeetingActor(
       // Guests
       case m: GetGuestsWaitingApprovalReqMsg           => handleGetGuestsWaitingApprovalReqMsg(m)
       case m: SetGuestPolicyCmdMsg                     => handleSetGuestPolicyMsg(m)
+      case m: SetGuestLobbyMessageCmdMsg               => handleSetGuestLobbyMessageMsg(m)
       case m: GuestsWaitingApprovedMsg                 => handleGuestsWaitingApprovedMsg(m)
       case m: GuestWaitingLeftMsg                      => handleGuestWaitingLeftMsg(m)
       case m: GetGuestPolicyReqMsg                     => handleGetGuestPolicyReqMsg(m)
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala
index 7c08cdfdfd49facb7e99a966b5a8934f213451b0..608f9c2d8436ae3ba2dda5b9bece331c8dc5a9d5 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala
@@ -139,6 +139,8 @@ class AnalyticsActor extends Actor with ActorLogging {
       case m: GuestsWaitingForApprovalEvtMsg => logMessage(msg)
       case m: SetGuestPolicyCmdMsg => logMessage(msg)
       case m: GuestPolicyChangedEvtMsg => logMessage(msg)
+      case m: SetGuestLobbyMessageCmdMsg => logMessage(msg)
+      case m: GuestLobbyMessageChangedEvtMsg => logMessage(msg)
 
       // System
       case m: ClientToServerLatencyTracerMsg => traceMessage(msg)
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/guests/SetGuestLobbyMessageMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/guests/SetGuestLobbyMessageMsgHdlr.scala
new file mode 100755
index 0000000000000000000000000000000000000000..dc5ad18a1dd089e89a9556287d16af9716379d5a
--- /dev/null
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/guests/SetGuestLobbyMessageMsgHdlr.scala
@@ -0,0 +1,31 @@
+package org.bigbluebutton.core2.message.handlers.guests
+
+import org.bigbluebutton.common2.msgs.SetGuestLobbyMessageCmdMsg
+import org.bigbluebutton.core.models.{ GuestsWaiting }
+import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
+import org.bigbluebutton.core2.message.senders.MsgBuilder
+import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
+import org.bigbluebutton.core.running.MeetingActor
+
+trait SetGuestLobbyMessageMsgHdlr extends RightsManagementTrait {
+  this: MeetingActor =>
+
+  val liveMeeting: LiveMeeting
+  val outGW: OutMsgRouter
+
+  def handleSetGuestLobbyMessageMsg(msg: SetGuestLobbyMessageCmdMsg): Unit = {
+    if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
+      val meetingId = liveMeeting.props.meetingProp.intId
+      val reason = "No permission to set guest lobby message in meeting."
+      PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
+    } else {
+      GuestsWaiting.setGuestLobbyMessage(liveMeeting.guestsWaiting, msg.body.message)
+      val event = MsgBuilder.buildGuestLobbyMessageChangedEvtMsg(
+        liveMeeting.props.meetingProp.intId,
+        msg.header.userId,
+        msg.body.message
+      )
+      outGW.send(event)
+    }
+  }
+}
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/MsgBuilder.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/MsgBuilder.scala
index 82f228180cd73256510f607f4604c81ea963f437..cebf928937da4b7fa477419c4ed6c1d2c729232c 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/MsgBuilder.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/MsgBuilder.scala
@@ -16,6 +16,17 @@ object MsgBuilder {
     BbbCommonEnvCoreMsg(envelope, event)
   }
 
+  def buildGuestLobbyMessageChangedEvtMsg(meetingId: String, userId: String, message: String): BbbCommonEnvCoreMsg = {
+    val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
+    val envelope = BbbCoreEnvelope(GuestLobbyMessageChangedEvtMsg.NAME, routing)
+    val header = BbbClientMsgHeader(GuestLobbyMessageChangedEvtMsg.NAME, meetingId, userId)
+
+    val body = GuestLobbyMessageChangedEvtMsgBody(message)
+    val event = GuestLobbyMessageChangedEvtMsg(header, body)
+
+    BbbCommonEnvCoreMsg(envelope, event)
+  }
+
   def buildGuestApprovedEvtMsg(meetingId: String, userId: String, status: String, approvedBy: String): BbbCommonEnvCoreMsg = {
     val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, meetingId, userId)
     val envelope = BbbCoreEnvelope(GuestApprovedEvtMsg.NAME, routing)
diff --git a/akka-bbb-fsesl/build.sbt b/akka-bbb-fsesl/build.sbt
index 99fca10117ecae327ad80b65566028539b327c80..6fbc2f7941670b9f8e614140513ecd62ae5e1eee 100755
--- a/akka-bbb-fsesl/build.sbt
+++ b/akka-bbb-fsesl/build.sbt
@@ -79,4 +79,4 @@ daemonGroup in Linux := group
 
 javaOptions in Universal ++= Seq("-J-Xms130m", "-J-Xmx256m", "-Dconfig.file=/etc/bigbluebutton/bbb-fsesl-akka.conf", "-Dlogback.configurationFile=conf/logback.xml")
 
-debianPackageDependencies in Debian ++= Seq("java8-runtime-headless", "bash")
+debianPackageDependencies in Debian ++= Seq("java8-runtime-headless", "bash", "bbb-freeswitch-core")
diff --git a/akka-bbb-fsesl/src/debian/DEBIAN/postinst b/akka-bbb-fsesl/src/debian/DEBIAN/postinst
new file mode 100644
index 0000000000000000000000000000000000000000..83db223d8e84a7b619733d17d167bbd2ca5e5b60
--- /dev/null
+++ b/akka-bbb-fsesl/src/debian/DEBIAN/postinst
@@ -0,0 +1,10 @@
+#
+# Update ESL password to match the password from bbb-freeswitch-core, which is
+# listed as a package dependency to ensure that it configures first.
+#
+
+ESL_PASSWORD=$(xmlstarlet sel -t -m 'configuration/settings/param[@name="password"]' -v @value /opt/freeswitch/etc/freeswitch/autoload_configs/event_socket.conf.xml)
+
+if [ -n "$ESL_PASSWORD" ]; then
+    sed -i "s/ClueCon/$ESL_PASSWORD/g" /etc/bigbluebutton/bbb-fsesl-akka.conf
+fi
diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/GuestsMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/GuestsMsgs.scala
index fd438b24b8d605bab9689b6eb26621c97b9cf703..308c481f56e0e76cea6c09dfc21f317d58138ae5 100755
--- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/GuestsMsgs.scala
+++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/GuestsMsgs.scala
@@ -103,6 +103,26 @@ case class GuestPolicyChangedEvtMsg(
 ) extends BbbCoreMsg
 case class GuestPolicyChangedEvtMsgBody(policy: String, setBy: String)
 
+/**
+ * Message from user to set the guest lobby message.
+ */
+object SetGuestLobbyMessageCmdMsg { val NAME = "SetGuestLobbyMessageCmdMsg" }
+case class SetGuestLobbyMessageCmdMsg(
+    header: BbbClientMsgHeader,
+    body:   SetGuestLobbyMessageCmdMsgBody
+) extends StandardMsg
+case class SetGuestLobbyMessageCmdMsgBody(message: String)
+
+/**
+ * Message sent to all clients that guest lobby message has been changed.
+ */
+object GuestLobbyMessageChangedEvtMsg { val NAME = "GuestLobbyMessageChangedEvtMsg" }
+case class GuestLobbyMessageChangedEvtMsg(
+    header: BbbClientMsgHeader,
+    body:   GuestLobbyMessageChangedEvtMsgBody
+) extends BbbCoreMsg
+case class GuestLobbyMessageChangedEvtMsgBody(message: String)
+
 /**
  * Message from user to get the guest policy.
  */
diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java
index 814736ff602308a1976dc87a2259286ac5b230d6..fad6f60344ca23bfedaf9f6495dab960ff98e8f5 100755
--- a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java
+++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java
@@ -61,6 +61,7 @@ import org.bigbluebutton.api.messaging.messages.CreateBreakoutRoom;
 import org.bigbluebutton.api.messaging.messages.CreateMeeting;
 import org.bigbluebutton.api.messaging.messages.EndMeeting;
 import org.bigbluebutton.api.messaging.messages.GuestPolicyChanged;
+import org.bigbluebutton.api.messaging.messages.GuestLobbyMessageChanged;
 import org.bigbluebutton.api.messaging.messages.GuestStatusChangedEventMsg;
 import org.bigbluebutton.api.messaging.messages.GuestsStatus;
 import org.bigbluebutton.api.messaging.messages.IMessage;
@@ -1101,6 +1102,8 @@ public class MeetingService implements MessageListener {
           processGuestStatusChangedEventMsg((GuestStatusChangedEventMsg) message);
         } else if (message instanceof GuestPolicyChanged) {
           processGuestPolicyChanged((GuestPolicyChanged) message);
+        } else if (message instanceof GuestLobbyMessageChanged) {
+          processGuestLobbyMessageChanged((GuestLobbyMessageChanged) message);
         } else if (message instanceof RecordChapterBreak) {
           processRecordingChapterBreak((RecordChapterBreak) message);
         } else if (message instanceof AddPad) {
@@ -1125,6 +1128,13 @@ public class MeetingService implements MessageListener {
     }
   }
 
+  public void processGuestLobbyMessageChanged(GuestLobbyMessageChanged msg) {
+    Meeting m = getMeeting(msg.meetingId);
+    if (m != null) {
+      m.setGuestLobbyMessage(msg.message);
+    }
+  }
+
   public void processAddPad(AddPad msg) {
     Meeting m = getMeeting(msg.meetingId);
     if (m != null) {
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 1393228d56674b7fe5ba1290d00b110eb6aa67fa..e2884db3445619a0dab5912b404e20d21cec0200 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
@@ -68,6 +68,7 @@ public class Meeting {
 	private String defaultAvatarURL;
 	private String defaultConfigToken;
 	private String guestPolicy = GuestPolicy.ASK_MODERATOR;
+	private String guestLobbyMessage = "";
 	private Boolean authenticatedGuest = false;
 	private boolean userHasJoined = false;
 	private Map<String, String> pads;
@@ -376,6 +377,14 @@ public class Meeting {
     	return guestPolicy;
 	}
 
+	public void setGuestLobbyMessage(String message) {
+		guestLobbyMessage = message;
+	}
+
+	public String getGuestLobbyMessage() {
+		return guestLobbyMessage;
+	}
+
 	public void setAuthenticatedGuest(Boolean authGuest) {
 		authenticatedGuest = authGuest;
 	}
diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/GuestLobbyMessageChanged.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/GuestLobbyMessageChanged.java
new file mode 100755
index 0000000000000000000000000000000000000000..ca76c835e877b1a894edfedd61877e5b570609bb
--- /dev/null
+++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/messaging/messages/GuestLobbyMessageChanged.java
@@ -0,0 +1,11 @@
+package org.bigbluebutton.api.messaging.messages;
+
+public class GuestLobbyMessageChanged implements IMessage {
+    public final String meetingId;
+    public final String message;
+
+    public GuestLobbyMessageChanged(String meetingId, String message) {
+        this.meetingId = meetingId;
+        this.message = message;
+    }
+}
diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala
index 7eb216f80f497677bae82e1fa9494eb9a1f4fa27..2676e1bdad6cdf96b529b68ae2e3812209bdf2d7 100755
--- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala
+++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/bus/ReceivedJsonMsgHdlrActor.scala
@@ -94,6 +94,8 @@ class ReceivedJsonMsgHdlrActor(val msgFromAkkaAppsEventBus: MsgFromAkkaAppsEvent
         route[GuestsWaitingApprovedEvtMsg](envelope, jsonNode)
       case GuestPolicyChangedEvtMsg.NAME =>
         route[GuestPolicyChangedEvtMsg](envelope, jsonNode)
+      case GuestLobbyMessageChangedEvtMsg.NAME =>
+        route[GuestLobbyMessageChangedEvtMsg](envelope, jsonNode)
       case AddPadEvtMsg.NAME =>
         route[AddPadEvtMsg](envelope, jsonNode)
       case AddCaptionsPadsEvtMsg.NAME =>
diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala
index 09fd1744a7250a3d2f49803f4b68b732b7eedaac..ac25f7610c30f77572dcbb1baec069bd9c3fe104 100755
--- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala
+++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala
@@ -39,6 +39,7 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW)
       case m: PresentationUploadTokenSysPubMsg  => handlePresentationUploadTokenSysPubMsg(m)
       case m: GuestsWaitingApprovedEvtMsg       => handleGuestsWaitingApprovedEvtMsg(m)
       case m: GuestPolicyChangedEvtMsg          => handleGuestPolicyChangedEvtMsg(m)
+      case m: GuestLobbyMessageChangedEvtMsg    => handleGuestLobbyMessageChangedEvtMsg(m)
       case m: AddCaptionsPadsEvtMsg             => handleAddCaptionsPadsEvtMsg(m)
       case m: AddPadEvtMsg                      => handleAddPadEvtMsg(m)
       case m: RecordingChapterBreakSysMsg       => handleRecordingChapterBreakSysMsg(m)
@@ -52,6 +53,10 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW)
     olgMsgGW.handle(new GuestPolicyChanged(msg.header.meetingId, msg.body.policy))
   }
 
+  def handleGuestLobbyMessageChangedEvtMsg(msg: GuestLobbyMessageChangedEvtMsg): Unit = {
+    olgMsgGW.handle(new GuestLobbyMessageChanged(msg.header.meetingId, msg.body.message))
+  }
+
   def handleAddPadEvtMsg(msg: AddPadEvtMsg): Unit = {
     olgMsgGW.handle(new AddPad(msg.header.meetingId, msg.body.padId, msg.body.readOnlyId))
   }
diff --git a/bigbluebutton-html5/.gitignore b/bigbluebutton-html5/.gitignore
index 3a833eaf4f800bfece09c1cd77ed8cd112714fc2..b8bd4a6520ec7750e5c0d810ffc70d79e9e3e98a 100755
--- a/bigbluebutton-html5/.gitignore
+++ b/bigbluebutton-html5/.gitignore
@@ -3,5 +3,7 @@ npm-debug.log
 node_modules/
 .meteor/dev_bundle
 tests/webdriverio/.testing-env
+public/locales/de_DE.json
+public/locales/ja_JP.json
 
 
diff --git a/bigbluebutton-html5/imports/api/guest-users/server/methods.js b/bigbluebutton-html5/imports/api/guest-users/server/methods.js
index dc7ccd93f0dea3c9a099a50a4b653a9c4b1734d2..f1dbcab3f7ea75c7ce149c8f1004a2f6ca31cf2e 100644
--- a/bigbluebutton-html5/imports/api/guest-users/server/methods.js
+++ b/bigbluebutton-html5/imports/api/guest-users/server/methods.js
@@ -1,8 +1,10 @@
 import { Meteor } from 'meteor/meteor';
 import allowPendingUsers from '/imports/api/guest-users/server/methods/allowPendingUsers';
 import changeGuestPolicy from '/imports/api/guest-users/server/methods/changeGuestPolicy';
+import setGuestLobbyMessage from '/imports/api/guest-users/server/methods/setGuestLobbyMessage';
 
 Meteor.methods({
   allowPendingUsers,
   changeGuestPolicy,
+  setGuestLobbyMessage,
 });
diff --git a/bigbluebutton-html5/imports/api/guest-users/server/methods/setGuestLobbyMessage.js b/bigbluebutton-html5/imports/api/guest-users/server/methods/setGuestLobbyMessage.js
new file mode 100644
index 0000000000000000000000000000000000000000..770ea415235e95c8c89260400628e1235e63f839
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/guest-users/server/methods/setGuestLobbyMessage.js
@@ -0,0 +1,24 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import RedisPubSub from '/imports/startup/server/redis';
+import Logger from '/imports/startup/server/logger';
+import { extractCredentials } from '/imports/api/common/server/helpers';
+
+const REDIS_CONFIG = Meteor.settings.private.redis;
+const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
+const EVENT_NAME = 'SetGuestLobbyMessageCmdMsg';
+
+export default function setGuestLobbyMessage(message) {
+  check(message, String);
+
+  const { meetingId, requesterUserId } = extractCredentials(this.userId);
+
+  check(meetingId, String);
+  check(requesterUserId, String);
+
+  const payload = { message };
+
+  Logger.info(`User=${requesterUserId} set guest lobby message to ${message}`);
+
+  return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
+}
diff --git a/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js b/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js
index 81f0ef206a42523e373fd5b1f58f316a19b870b6..e13e7c5f2da353bd9835b1067186ad5c617d7c25 100644
--- a/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js
+++ b/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js
@@ -4,6 +4,7 @@ import handleGetAllMeetings from './handlers/getAllMeetings';
 import handleMeetingEnd from './handlers/meetingEnd';
 import handleMeetingDestruction from './handlers/meetingDestruction';
 import handleMeetingLocksChange from './handlers/meetingLockChange';
+import handleGuestLobbyMessageChanged from './handlers/guestLobbyMessageChanged';
 import handleUserLockChange from './handlers/userLockChange';
 import handleRecordingStatusChange from './handlers/recordingStatusChange';
 import handleRecordingTimerChange from './handlers/recordingTimerChange';
@@ -21,5 +22,6 @@ RedisPubSub.on('RecordingStatusChangedEvtMsg', handleRecordingStatusChange);
 RedisPubSub.on('UpdateRecordingTimerEvtMsg', handleRecordingTimerChange);
 RedisPubSub.on('WebcamsOnlyForModeratorChangedEvtMsg', handleChangeWebcamOnlyModerator);
 RedisPubSub.on('GetLockSettingsRespMsg', handleMeetingLocksChange);
+RedisPubSub.on('GuestLobbyMessageChangedEvtMsg', handleGuestLobbyMessageChanged);
 RedisPubSub.on('MeetingTimeRemainingUpdateEvtMsg', handleTimeRemainingUpdate);
 RedisPubSub.on('SelectRandomViewerRespMsg', handleSelectRandomViewer);
diff --git a/bigbluebutton-html5/imports/api/meetings/server/handlers/guestLobbyMessageChanged.js b/bigbluebutton-html5/imports/api/meetings/server/handlers/guestLobbyMessageChanged.js
new file mode 100644
index 0000000000000000000000000000000000000000..e6bca30ecfedd3f20b4286fa0b6e79086a198f72
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/meetings/server/handlers/guestLobbyMessageChanged.js
@@ -0,0 +1,11 @@
+import setGuestLobbyMessage from '../modifiers/setGuestLobbyMessage';
+import { check } from 'meteor/check';
+
+export default function handleGuestLobbyMessageChanged({ body }, meetingId) {
+  const { message } = body;
+
+  check(meetingId, String);
+  check(message, String);
+
+  return setGuestLobbyMessage(meetingId, message);
+}
diff --git a/bigbluebutton-html5/imports/api/meetings/server/handlers/meetingEnd.js b/bigbluebutton-html5/imports/api/meetings/server/handlers/meetingEnd.js
index d1e7feed8d546b4232ade9bd321621ccb6bd5fa6..48e29bef3607186454f0afcc22cc73c7f927af49 100644
--- a/bigbluebutton-html5/imports/api/meetings/server/handlers/meetingEnd.js
+++ b/bigbluebutton-html5/imports/api/meetings/server/handlers/meetingEnd.js
@@ -4,11 +4,15 @@ import Meetings from '/imports/api/meetings';
 import Breakouts from '/imports/api/breakouts';
 import Logger from '/imports/startup/server/logger';
 
-export default function handleMeetingEnd({ body }) {
+export default function handleMeetingEnd({ header, body }) {
   check(body, Object);
   const { meetingId } = body;
   check(meetingId, String);
 
+  check(header, Object);
+  const { userId } = header;
+  check(userId, String);
+
   const cb = (err, num, meetingType) => {
     if (err) {
       Logger.error(`${meetingType} ending error: ${err}`);
@@ -20,7 +24,7 @@ export default function handleMeetingEnd({ body }) {
   };
 
   Meetings.update({ meetingId },
-    { $set: { meetingEnded: true } },
+    { $set: { meetingEnded: true, meetingEndedBy: userId } },
     (err, num) => { cb(err, num, 'Meeting'); });
 
   Breakouts.update({ parentMeetingId: meetingId },
diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js
index a17bec63f2b851c3e0b2c3305371cda8fff4ce4d..a0dea94a0a5b9fd6913cfe5d6865837ca1a7ec33 100755
--- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js
+++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js
@@ -148,6 +148,7 @@ export default function addMeeting(meeting) {
       meetingId,
       meetingEnded,
       publishedPoll: false,
+      guestLobbyMessage: '',
       randomlySelectedUser: '',
     }, flat(newMeeting, {
       safe: true,
diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/setGuestLobbyMessage.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/setGuestLobbyMessage.js
new file mode 100644
index 0000000000000000000000000000000000000000..08650ed27d4475259c813eb62b01e25eb28e9037
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/setGuestLobbyMessage.js
@@ -0,0 +1,28 @@
+import Meetings from '/imports/api/meetings';
+import Logger from '/imports/startup/server/logger';
+import { check } from 'meteor/check';
+
+export default function setGuestLobbyMessage(meetingId, guestLobbyMessage) {
+  check(meetingId, String);
+  check(guestLobbyMessage, String);
+
+  const selector = {
+    meetingId,
+  };
+
+  const modifier = {
+    $set: {
+      guestLobbyMessage,
+    },
+  };
+
+  try {
+    const { numberAffected } = Meetings.upsert(selector, modifier);
+
+    if (numberAffected) {
+      Logger.verbose(`Set guest lobby message meetingId=${meetingId} guestLobbyMessage=${guestLobbyMessage}`);
+    }
+  } catch (err) {
+    Logger.error(`Setting guest lobby message: ${err}`);
+  }
+}
diff --git a/bigbluebutton-html5/imports/startup/client/intl.jsx b/bigbluebutton-html5/imports/startup/client/intl.jsx
index 0d44ca2d67522c69ce2c6bcefdf016f449523f8f..c56fac96baa696728478bc9e458028b0b98e5016 100644
--- a/bigbluebutton-html5/imports/startup/client/intl.jsx
+++ b/bigbluebutton-html5/imports/startup/client/intl.jsx
@@ -63,6 +63,7 @@ class IntlStartup extends Component {
 
   fetchLocalizedMessages(locale, init = false) {
     const url = `./locale?locale=${locale}&init=${init}`;
+    const localesPath = 'locales';
 
     this.setState({ fetching: true }, () => {
       fetch(url)
@@ -73,11 +74,65 @@ class IntlStartup extends Component {
 
           return response.json();
         })
-        .then(({ messages, normalizedLocale }) => {
-          const dasherizedLocale = normalizedLocale.replace('_', '-');
-          this.setState({ messages, fetching: false, normalizedLocale: dasherizedLocale }, () => {
-            IntlStartup.saveLocale(dasherizedLocale);
-          });
+        .then(({ normalizedLocale, regionDefaultLocale }) => {
+          fetch(`${localesPath}/${DEFAULT_LANGUAGE}.json`)
+            .then((response) => {
+              if (!response.ok) {
+                return Promise.reject();
+              }
+              return response.json();
+            })
+            .then((messages) => {
+              if (regionDefaultLocale !== '') {
+                fetch(`${localesPath}/${regionDefaultLocale}.json`)
+                  .then((response) => {
+                    if (!response.ok) {
+                      return Promise.resolve();
+                    }
+                    return response.json();
+                  })
+                  .then((regionDefaultMessages) => {
+                    messages = Object.assign(messages, regionDefaultMessages);
+                    return messages;
+                  });
+              }
+
+              if (normalizedLocale !== DEFAULT_LANGUAGE && normalizedLocale !== regionDefaultLocale) {
+                fetch(`${localesPath}/${normalizedLocale}.json`)
+                  .then((response) => {
+                    if (!response.ok) {
+                      return Promise.reject();
+                    }
+                    return response.json();
+                  })
+                  .then((localeMessages) => {
+                    messages = Object.assign(messages, localeMessages);
+                    return messages;
+                  })
+                  .catch(() => {
+                    normalizedLocale = (regionDefaultLocale) || DEFAULT_LANGUAGE;
+                    const dasherizedLocale = normalizedLocale.replace('_', '-');
+                    this.setState({ messages, fetching: false, normalizedLocale: dasherizedLocale }, () => {
+                      IntlStartup.saveLocale(normalizedLocale);
+                    });
+                  });
+              }
+
+              return messages;
+            })
+            .then((messages) => {
+              const dasherizedLocale = normalizedLocale.replace('_', '-');
+              this.setState({ messages, fetching: false, normalizedLocale: dasherizedLocale }, () => {
+                IntlStartup.saveLocale(dasherizedLocale);
+              });
+            })
+            .catch(() => {
+              normalizedLocale = DEFAULT_LANGUAGE;
+              const dasherizedLocale = normalizedLocale.replace('_', '-');
+              this.setState({ fetching: false, normalizedLocale: dasherizedLocale }, () => {
+                IntlStartup.saveLocale(normalizedLocale);
+              });
+            });
         })
         .catch(() => {
           this.setState({ fetching: false, normalizedLocale: null }, () => {
diff --git a/bigbluebutton-html5/imports/startup/server/index.js b/bigbluebutton-html5/imports/startup/server/index.js
index 7ad0f829dd88987b6f9723b24333698fd15aefb7..5f6508e6a32a1c6ac6d7cf89defff9dae9f2a45d 100755
--- a/bigbluebutton-html5/imports/startup/server/index.js
+++ b/bigbluebutton-html5/imports/startup/server/index.js
@@ -12,7 +12,16 @@ import Redis from './redis';
 import setMinBrowserVersions from './minBrowserVersion';
 
 let guestWaitHtml = '';
-const AVAILABLE_LOCALES = fs.readdirSync('assets/app/locales');
+
+const env = Meteor.isDevelopment ? 'development' : 'production';
+
+const meteorRoot = fs.realpathSync(`${process.cwd()}/../`);
+
+const applicationRoot = (env === 'development')
+  ? fs.realpathSync(`${meteorRoot}'/../../../../public/locales/`)
+  : fs.realpathSync(`${meteorRoot}/../programs/web.browser/app/locales/`);
+
+const AVAILABLE_LOCALES = fs.readdirSync(`${applicationRoot}`);
 const FALLBACK_LOCALES = JSON.parse(Assets.getText('config/fallbackLocales.json'));
 
 process.on('uncaughtException', (err) => {
@@ -27,7 +36,6 @@ process.on('uncaughtException', (err) => {
 
 Meteor.startup(() => {
   const APP_CONFIG = Meteor.settings.public.app;
-  const env = Meteor.isDevelopment ? 'development' : 'production';
   const CDN_URL = APP_CONFIG.cdn;
   const instanceId = parseInt(process.env.INSTANCE_ID, 10) || 1;
 
@@ -106,40 +114,39 @@ Meteor.startup(() => {
         session.bbbFixApplied = true;
       }
     }, 5000);
+  }
+  if (CDN_URL.trim()) {
+    // Add CDN
+    BrowserPolicy.content.disallowEval();
+    BrowserPolicy.content.allowInlineScripts();
+    BrowserPolicy.content.allowInlineStyles();
+    BrowserPolicy.content.allowImageDataUrl(CDN_URL);
+    BrowserPolicy.content.allowFontDataUrl(CDN_URL);
+    BrowserPolicy.content.allowOriginForAll(CDN_URL);
+    WebAppInternals.setBundledJsCssPrefix(CDN_URL + APP_CONFIG.basename + Meteor.settings.public.app.instanceId);
+
+    const fontRegExp = /\.(eot|ttf|otf|woff|woff2)$/;
+
+    WebApp.rawConnectHandlers.use('/', (req, res, next) => {
+      if (fontRegExp.test(req._parsedUrl.pathname)) {
+        res.setHeader('Access-Control-Allow-Origin', '*');
+        res.setHeader('Vary', 'Origin');
+        res.setHeader('Pragma', 'public');
+        res.setHeader('Cache-Control', '"public"');
+      }
+      return next();
+    });
+  }
 
-    if (CDN_URL.trim()) {
-      // Add CDN
-      BrowserPolicy.content.disallowEval();
-      BrowserPolicy.content.allowInlineScripts();
-      BrowserPolicy.content.allowInlineStyles();
-      BrowserPolicy.content.allowImageDataUrl(CDN_URL);
-      BrowserPolicy.content.allowFontDataUrl(CDN_URL);
-      BrowserPolicy.content.allowOriginForAll(CDN_URL);
-      WebAppInternals.setBundledJsCssPrefix(CDN_URL + APP_CONFIG.basename + Meteor.settings.public.app.instanceId);
-
-      const fontRegExp = /\.(eot|ttf|otf|woff|woff2)$/;
-
-      WebApp.rawConnectHandlers.use('/', (req, res, next) => {
-        if (fontRegExp.test(req._parsedUrl.pathname)) {
-          res.setHeader('Access-Control-Allow-Origin', '*');
-          res.setHeader('Vary', 'Origin');
-          res.setHeader('Pragma', 'public');
-          res.setHeader('Cache-Control', '"public"');
-        }
-        return next();
-      });
-    }
-
-    setMinBrowserVersions();
+  setMinBrowserVersions();
 
-    Logger.warn(`SERVER STARTED.
-    ENV=${env}
-    nodejs version=${process.version}
-    BBB_HTML5_ROLE=${process.env.BBB_HTML5_ROLE}
-    INSTANCE_ID=${instanceId}
-    PORT=${process.env.PORT}
-    CDN=${CDN_URL}\n`, APP_CONFIG);
-  }
+  Logger.warn(`SERVER STARTED.
+  ENV=${env}
+  nodejs version=${process.version}
+  BBB_HTML5_ROLE=${process.env.BBB_HTML5_ROLE}
+  INSTANCE_ID=${instanceId}
+  PORT=${process.env.PORT}
+  CDN=${CDN_URL}\n`, APP_CONFIG);
 });
 
 
@@ -192,7 +199,7 @@ WebApp.connectHandlers.use('/locale', (req, res) => {
   const browserLocale = override && req.query.init === 'true'
     ? override.split(/[-_]/g) : req.query.locale.split(/[-_]/g);
 
-  const localeList = [fallback];
+  let localeFile = fallback;
 
   const usableLocales = AVAILABLE_LOCALES
     .map(file => file.replace('.json', ''))
@@ -200,35 +207,29 @@ WebApp.connectHandlers.use('/locale', (req, res) => {
       ? [...locales, locale]
       : locales), []);
 
-  const regionDefault = usableLocales.find(locale => browserLocale[0] === locale);
-
-  if (regionDefault) localeList.push(regionDefault);
-  if (!regionDefault && usableLocales.length) localeList.push(usableLocales[0]);
-
   let normalizedLocale;
-  let messages = {};
 
   if (browserLocale.length > 1) {
     normalizedLocale = `${browserLocale[0]}_${browserLocale[1].toUpperCase()}`;
-    localeList.push(normalizedLocale);
+
+    const normDefault = usableLocales.find(locale => normalizedLocale === locale);
+    if (normDefault) localeFile = normDefault;
   }
 
-  localeList.forEach((locale) => {
-    try {
-      const data = Assets.getText(`locales/${locale}.json`);
-      messages = Object.assign(messages, JSON.parse(data));
-      normalizedLocale = locale;
-    } catch (e) {
-      Logger.info(`'Could not process locale ${locale}:${e}`);
-      // Getting here means the locale is not available in the current locale files.
-    }
-  });
+  const regionDefault = usableLocales.find(locale => browserLocale[0] === locale);
+
+  if (localeFile === fallback && regionDefault !== localeFile) {
+    localeFile = regionDefault;
+  }
 
   res.setHeader('Content-Type', 'application/json');
-  res.end(JSON.stringify({ normalizedLocale, messages }));
+  res.end(JSON.stringify({
+    normalizedLocale: localeFile,
+    regionDefaultLocale: (regionDefault && regionDefault !== localeFile) ? regionDefault : '',
+  }));
 });
 
-WebApp.connectHandlers.use('/locales', (req, res) => {
+WebApp.connectHandlers.use('/locale-list', (req, res) => {
   if (!avaibleLocalesNamesJSON) {
     avaibleLocalesNamesJSON = JSON.stringify(generateLocaleOptions());
   }
diff --git a/bigbluebutton-html5/imports/ui/components/layout/layout-manager.jsx b/bigbluebutton-html5/imports/ui/components/layout/layout-manager.jsx
index 3e5e63c6007e3cc1d3cb789b696bb7aa4f567ccc..938b7dde7df4c3b894d19734278d4ddf402c2f9b 100644
--- a/bigbluebutton-html5/imports/ui/components/layout/layout-manager.jsx
+++ b/bigbluebutton-html5/imports/ui/components/layout/layout-manager.jsx
@@ -307,11 +307,11 @@ class LayoutManager extends Component {
     let storageBreakoutRoomWidth;
 
     if (storageLData) {
-      storageUserListWidth = storageLData.userListSize.width;
-      storageChatWidth = storageLData.chatSize.width;
-      storagePollWidth = storageLData.pollSize.width;
-      storageNoteWidth = storageLData.noteSize.width;
-      storageBreakoutRoomWidth = storageLData.breakoutRoomSize.width;
+      storageUserListWidth = storageLData.userListSize?.width;
+      storageChatWidth = storageLData.chatSize?.width;
+      storagePollWidth = storageLData.pollSize?.width;
+      storageNoteWidth = storageLData.noteSize?.width;
+      storageBreakoutRoomWidth = storageLData.breakoutRoomSize?.width;
     }
 
     let newUserListSize;
diff --git a/bigbluebutton-html5/imports/ui/components/legacy/component.jsx b/bigbluebutton-html5/imports/ui/components/legacy/component.jsx
index f6c049f3452dc5cc425822c770f4ba119e834c2d..b859270bd6d4c88bf16b98e5fec0126d5cf2b665 100755
--- a/bigbluebutton-html5/imports/ui/components/legacy/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/legacy/component.jsx
@@ -69,16 +69,17 @@ const FETCHING = 'fetching';
 const FALLBACK = 'fallback';
 const READY = 'ready';
 const supportedBrowsers = ['chrome', 'firefox', 'safari', 'opera', 'edge', 'yandex'];
+const DEFAULT_LANGUAGE = Meteor.settings.public.app.defaultSettings.application.fallbackLocale;
 
 export default class Legacy extends Component {
   constructor(props) {
     super(props);
 
     const locale = navigator.languages ? navigator.languages[0] : false
-      || navigator.language
-      || Meteor.settings.public.app.defaultSettings.application.fallbackLocale;
+      || navigator.language;
 
     const url = `./locale?locale=${locale}`;
+    const localesPath = 'locales';
 
     const that = this;
     this.state = { viewState: FETCHING };
@@ -90,9 +91,56 @@ export default class Legacy extends Component {
 
         return response.json();
       })
-      .then(({ messages, normalizedLocale }) => {
-        const dasherizedLocale = normalizedLocale.replace('_', '-');
-        that.setState({ messages, normalizedLocale: dasherizedLocale, viewState: READY });
+      .then(({ normalizedLocale, regionDefaultLocale }) => {
+        fetch(`${localesPath}/${DEFAULT_LANGUAGE}.json`)
+          .then((response) => {
+            if (!response.ok) {
+              return Promise.reject();
+            }
+            return response.json();
+          })
+          .then((messages) => {
+            if (regionDefaultLocale !== '') {
+              fetch(`${localesPath}/${regionDefaultLocale}.json`)
+                .then((response) => {
+                  if (!response.ok) {
+                    return Promise.resolve();
+                  }
+                  return response.json();
+                })
+                .then((regionDefaultMessages) => {
+                  messages = Object.assign(messages, regionDefaultMessages);
+                  this.setState({ messages});
+                });
+            }
+
+            if (normalizedLocale && normalizedLocale !== DEFAULT_LANGUAGE && normalizedLocale !== regionDefaultLocale) {
+              fetch(`${localesPath}/${normalizedLocale}.json`)
+                .then((response) => {
+                  if (!response.ok) {
+                    return Promise.reject();
+                  }
+                  return response.json();
+                })
+                .then((localeMessages) => {
+                  messages = Object.assign(messages, localeMessages);
+                  this.setState({ messages});
+                })
+                .catch(() => {
+                  normalizedLocale = (regionDefaultLocale) || DEFAULT_LANGUAGE;
+                  const dasherizedLocale = normalizedLocale.replace('_', '-');
+                  this.setState({ messages, normalizedLocale: dasherizedLocale, viewState: READY });
+                });
+            }
+            return messages;
+          })
+          .then((messages) => {
+            const dasherizedLocale = normalizedLocale.replace('_', '-');
+            this.setState({ messages, normalizedLocale: dasherizedLocale, viewState: READY });
+          })
+          .catch(() => {
+            that.setState({ viewState: FALLBACK });
+          });
       })
       .catch(() => {
         that.setState({ viewState: FALLBACK });
diff --git a/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx b/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx
index a47575fcd678a49504151ef6deb05c5d0257c27b..4c9cf5a67b3915746a348d3e62737ccb0b79f445 100644
--- a/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx
@@ -489,6 +489,7 @@ class WebcamDraggable extends PureComponent {
             style={{
               marginLeft: 0,
               marginRight: 0,
+              zIndex: 2,
               display: hideWebcams ? 'none' : undefined,
             }}
           >
diff --git a/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx b/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx
index 647db1a3634e68756f6bd25d85faee3e169cb038..2878782e89be5212acca7d0270ba5981fc9976cc 100755
--- a/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx
@@ -11,6 +11,7 @@ import Rating from './rating/component';
 import { styles } from './styles';
 import logger from '/imports/startup/client/logger';
 import Users from '/imports/api/users';
+import Meetings from '/imports/api/meetings';
 import AudioManager from '/imports/ui/services/audio-manager';
 import { meetingIsBreakout } from '/imports/ui/components/app/service';
 
@@ -35,6 +36,10 @@ const intlMessage = defineMessages({
     id: 'app.meeting.endedMessage',
     description: 'message saying to go back to home screen',
   },
+  messageEndedByUser: {
+    id: 'app.meeting.endedByUserMessage',
+    description: 'message informing who ended the meeting',
+  },
   buttonOkay: {
     id: 'app.meeting.endNotification.ok.label',
     description: 'label okay for button',
@@ -116,6 +121,17 @@ class MeetingEnded extends PureComponent {
       this.localUserRole = user.role;
     }
 
+    const meeting = Meetings.findOne({ id: user.meetingID });
+    if (meeting) {
+      const endedBy = Users.findOne({
+        userId: meeting.meetingEndedBy,
+      }, { fields: { name: 1 } });
+
+      if (endedBy) {
+        this.meetingEndedBy = endedBy.name;
+      }
+    }
+
     this.setSelectedStar = this.setSelectedStar.bind(this);
     this.confirmRedirect = this.confirmRedirect.bind(this);
     this.sendFeedback = this.sendFeedback.bind(this);
@@ -209,6 +225,11 @@ class MeetingEnded extends PureComponent {
             </h1>
             {!allowRedirectToLogoutURL() ? null : (
               <div>
+                {this.meetingEndedBy ? (
+                  <div className={styles.text}>
+                    {intl.formatMessage(intlMessage.messageEndedByUser, { 0: this.meetingEndedBy })}
+                  </div>
+                ) : null}
                 <div className={styles.text}>
                   {intl.formatMessage(intlMessage.messageEnded)}
                 </div>
diff --git a/bigbluebutton-html5/imports/ui/components/poll/component.jsx b/bigbluebutton-html5/imports/ui/components/poll/component.jsx
index 479238daf3a535469deafb983858a2435b24680a..5ff55f15e9d32531557f0cfc17b9684f67823e0d 100644
--- a/bigbluebutton-html5/imports/ui/components/poll/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/poll/component.jsx
@@ -9,6 +9,7 @@ import cx from 'classnames';
 import Button from '/imports/ui/components/button/component';
 import LiveResult from './live-result/component';
 import { styles } from './styles.scss';
+import DragAndDrop from './dragAndDrop/component';
 
 const intlMessages = defineMessages({
   pollPaneTitle: {
@@ -31,6 +32,10 @@ const intlMessages = defineMessages({
     id: 'app.poll.activePollInstruction',
     description: 'instructions displayed when a poll is active',
   },
+  dragDropPollInstruction: {
+    id: 'app.poll.dragDropPollInstruction',
+    description: 'instructions for upload poll options via drag and drop',
+  },
   ariaInputCount: {
     id: 'app.poll.ariaInputCount',
     description: 'aria label for custom poll input field',
@@ -148,6 +153,7 @@ const intlMessages = defineMessages({
 const CHAT_ENABLED = Meteor.settings.public.chat.enabled;
 const MAX_CUSTOM_FIELDS = Meteor.settings.public.poll.max_custom;
 const MAX_INPUT_CHARS = 45;
+const FILE_DRAG_AND_DROP_ENABLED = Meteor.settings.public.poll.allowDragAndDropFile;
 
 const validateInput = (i) => {
   let _input = i;
@@ -206,6 +212,21 @@ class Poll extends Component {
     });
   }
 
+  handleInputChange(index, event) {
+    this.handleInputTextChange(index, event.target.value);
+  }
+
+  handleInputTextChange(index, text) {
+    const { optList } = this.state;
+    // This regex will replace any instance of 2 or more consecutive white spaces
+    // with a single white space character.
+    const option = text.replace(/\s{2,}/g, ' ').trim();
+
+    if (index < optList.length) optList[index].val = option === '' ? '' : option;
+
+    this.setState({ optList });
+  }
+
   handleInputChange(e, index) {
     const { optList, type, error } = this.state;
     const list = [...optList];
@@ -222,6 +243,24 @@ class Poll extends Component {
     this.setState({ question: validateInput(e.target.value), error: clearError ? null : error });
   }
 
+  pushToCustomPollValues(text) {
+    const lines = text.split('\n');
+    for (let i = 0; i < MAX_CUSTOM_FIELDS; i += 1) {
+      let line = '';
+      if (i < lines.length) {
+        line = lines[i];
+        line = line.length > MAX_INPUT_CHARS ? line.substring(0, MAX_INPUT_CHARS) : line;
+      }
+      this.handleInputTextChange(i, line);
+    }
+  }
+
+  handlePollValuesText(text) {
+    if (text && text.length > 0) {
+      this.pushToCustomPollValues(text);
+    }
+  }
+
   handleRemoveOption(index) {
     const { optList } = this.state;
     const list = [...optList];
@@ -496,6 +535,9 @@ class Poll extends Component {
                           });
                         }}
                       />
+                      {
+                        FILE_DRAG_AND_DROP_ENABLED && this.renderDragDrop()
+                      }
                     </div>
                     )
               }
@@ -537,6 +579,25 @@ class Poll extends Component {
     return this.renderPollOptions();
   }
 
+
+  renderDragDrop() {
+    const { intl } = this.props;
+    return (
+      <div>
+        <div className={styles.instructions}>
+          {intl.formatMessage(intlMessages.dragDropPollInstruction)}
+        </div>
+        <DragAndDrop
+          {...{ intl, MAX_INPUT_CHARS }}
+          handlePollValuesText={e => this.handlePollValuesText(e)}
+        >
+          <div className={styles.dragAndDropPollContainer} />
+        </DragAndDrop>
+      </div>
+    );
+  }
+
+
   render() {
     const {
       intl,
diff --git a/bigbluebutton-html5/imports/ui/components/poll/dragAndDrop/component.jsx b/bigbluebutton-html5/imports/ui/components/poll/dragAndDrop/component.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..b31318d63f85cf7ac179fb68bf256a5796513ca4
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/poll/dragAndDrop/component.jsx
@@ -0,0 +1,143 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { withModalMounter } from '/imports/ui/components/modal/service';
+
+import { defineMessages, injectIntl } from 'react-intl';
+import { styles } from './styles.scss';
+import Button from '/imports/ui/components/button/component';
+
+
+// src: https://medium.com/@650egor/simple-drag-and-drop-file-upload-in-react-2cb409d88929
+
+const intlMessages = defineMessages({
+  customPollTextArea: {
+    id: 'app.poll.customPollTextArea',
+    description: 'label for button to submit custom poll values',
+  },
+});
+
+class DragAndDrop extends Component {
+  static handleDrag(e) {
+    e.preventDefault();
+    e.stopPropagation();
+  }
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      drag: false,
+      pollValueText: '',
+    };
+
+    this.dropRef = React.createRef();
+  }
+
+  componentDidMount() {
+    this.dragCounter = 0;
+    const div = this.dropRef.current;
+    div.addEventListener('dragenter', e => this.handleDragIn(e));
+    div.addEventListener('dragleave', e => this.handleDragOut(e));
+    div.addEventListener('dragover', e => DragAndDrop.handleDrag(e));
+    div.addEventListener('drop', e => this.handleDrop(e));
+  }
+
+  componentWillUnmount() {
+    const div = this.dropRef.current;
+    div.removeEventListener('dragenter', e => this.handleDragIn(e));
+    div.removeEventListener('dragleave', e => this.handleDragOut(e));
+    div.removeEventListener('dragover', e => DragAndDrop.handleDrag(e));
+    div.removeEventListener('drop', e => this.handleDrop(e));
+  }
+
+  setPollValues() {
+    const { pollValueText } = this.state;
+    const { handlePollValuesText } = this.props;
+    if (pollValueText) {
+      handlePollValuesText(pollValueText);
+    }
+  }
+
+  setPollValuesFromFile(file) {
+    const reader = new FileReader();
+    reader.onload = async (e) => {
+      const text = e.target.result;
+      this.setPollValueText(text);
+      this.setPollValues();
+    };
+    reader.readAsText(file);
+  }
+
+  setPollValueText(pollText) {
+    const { MAX_INPUT_CHARS } = this.props;
+    const arr = pollText.split('\n');
+    const text = arr.map(line => (line.length > MAX_INPUT_CHARS ? line.substring(0, MAX_INPUT_CHARS) : line)).join('\n');
+    this.setState({ pollValueText: text });
+  }
+
+
+  handleTextInput(e) {
+    this.setPollValueText(e.target.value);
+  }
+
+
+  handleDragIn(e) {
+    DragAndDrop.handleDrag(e);
+    this.dragCounter += 1;
+    if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
+      this.setState({ drag: true });
+    }
+  }
+
+  handleDragOut(e) {
+    DragAndDrop.handleDrag(e);
+    this.dragCounter -= 1;
+    if (this.dragCounter > 0) return;
+    this.setState({ drag: false });
+  }
+
+  handleDrop(e) {
+    DragAndDrop.handleDrag(e);
+    this.setState({ drag: false });
+    if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
+      this.setPollValuesFromFile(e.dataTransfer.files[0]);
+      this.dragCounter = 0;
+    }
+  }
+
+
+  render() {
+    const { intl, children } = this.props;
+    const { pollValueText, drag } = this.state;
+    return (
+      <div
+        className={styles.dndContainer}
+        ref={this.dropRef}
+      >
+        <textarea
+          value={pollValueText}
+          className={drag ? styles.dndActive : styles.dndInActive}
+          onChange={e => this.handleTextInput(e)}
+        />
+        <Button
+          onClick={() => this.setPollValues()}
+          label={intl.formatMessage(intlMessages.customPollTextArea)}
+          color="primary"
+          disabled={pollValueText < 1}
+          className={styles.btn}
+        />
+        {children}
+      </div>
+
+    );
+  }
+} export default withModalMounter(injectIntl(DragAndDrop));
+
+DragAndDrop.propTypes = {
+  intl: PropTypes.shape({
+    formatMessage: PropTypes.func.isRequired,
+  }).isRequired,
+  MAX_INPUT_CHARS: PropTypes.number.isRequired,
+  handlePollValuesText: PropTypes.func.isRequired,
+  children: PropTypes.element.isRequired,
+};
diff --git a/bigbluebutton-html5/imports/ui/components/poll/dragAndDrop/styles.scss b/bigbluebutton-html5/imports/ui/components/poll/dragAndDrop/styles.scss
new file mode 100755
index 0000000000000000000000000000000000000000..abd06217ecf4e5d8ce4d9b48333dfc7ab4eb1b12
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/poll/dragAndDrop/styles.scss
@@ -0,0 +1,26 @@
+@import "/imports/ui/stylesheets/variables/_all";
+
+.dndContainer {
+  height: 200px;
+}
+
+.customPollValuesTextfield {
+  width: 100%;
+  height: 100%;
+  resize: none;
+  font-size: var(--font-size-small);
+}
+
+.dndActive {
+  @extend .customPollValuesTextfield;
+  background: grey;
+}
+
+.dndInActive {
+  @extend .customPollValuesTextfield;
+  background: white;
+}
+
+.btn {
+  width: 100%;
+}
diff --git a/bigbluebutton-html5/imports/ui/components/poll/styles.scss b/bigbluebutton-html5/imports/ui/components/poll/styles.scss
index d2e1953cdb266325c91c8c6d45ced32528841697..775d7d23a23700f4938566666300363e47b9f21a 100644
--- a/bigbluebutton-html5/imports/ui/components/poll/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/poll/styles.scss
@@ -356,3 +356,8 @@
         color: var(--color-white) !important;
     }
 }
+
+.dragAndDropPollContainer {
+    width: 200px !important;
+    height: 200px !important;
+}
diff --git a/bigbluebutton-html5/imports/ui/components/settings/service.js b/bigbluebutton-html5/imports/ui/components/settings/service.js
index 85af45dddd425ef6af2f48f04cd3115870e57262..42698cb30c268933700445e9841c7c8ee30ad0a4 100644
--- a/bigbluebutton-html5/imports/ui/components/settings/service.js
+++ b/bigbluebutton-html5/imports/ui/components/settings/service.js
@@ -27,7 +27,7 @@ const updateSettings = (obj, msg) => {
   }
 };
 
-const getAvailableLocales = () => fetch('./locales').then(locales => locales.json());
+const getAvailableLocales = () => fetch('./locale-list').then(locales => locales.json());
 
 export {
   getUserRoles,
diff --git a/bigbluebutton-html5/imports/ui/components/text-input/component.jsx b/bigbluebutton-html5/imports/ui/components/text-input/component.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..d3a1a3d1ebd9515071288515c33553ec4c48e967
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/text-input/component.jsx
@@ -0,0 +1,81 @@
+import React, { PureComponent } from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import TextareaAutosize from 'react-autosize-textarea';
+import PropTypes from 'prop-types';
+import logger from '/imports/startup/client/logger';
+import Button from '/imports/ui/components/button/component';
+import { styles } from './styles.scss';
+
+const propTypes = {
+  placeholder: PropTypes.string,
+  send: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+  placeholder: '',
+  send: () => logger.warn({ logCode: 'text_input_send_function' }, `Missing`),
+};
+
+const messages = defineMessages({
+  sendLabel: {
+    id: 'app.textInput.sendLabel',
+    description: 'Text input send button label',
+  },
+});
+
+class TextInput extends PureComponent {
+  constructor(props) {
+    super(props);
+
+    this.state = { message: '' };
+  }
+
+  handleOnChange(e) {
+    const message = e.target.value;
+    this.setState({ message });
+  }
+
+  handleOnClick() {
+    const { send } = this.props;
+    const { message } = this.state;
+
+    send(message);
+    this.setState({ message: '' });
+  }
+
+  render() {
+    const {
+      intl,
+      maxLength,
+      placeholder,
+    } = this.props;
+
+    const { message } = this.state;
+
+    return (
+      <div className={styles.wrapper}>
+        <TextareaAutosize
+          className={styles.textarea}
+          maxLength={maxLength}
+          onChange={(e) => this.handleOnChange(e)}
+          placeholder={placeholder}
+          value={message}
+        />
+        <Button
+          circle
+          className={styles.button}
+          color="primary"
+          hideLabel
+          icon="send"
+          label={intl.formatMessage(messages.sendLabel)}
+          onClick={() => this.handleOnClick()}
+        />
+      </div>
+    );
+  }
+}
+
+TextInput.propTypes = propTypes;
+TextInput.defaultProps = defaultProps;
+
+export default injectIntl(TextInput);
diff --git a/bigbluebutton-html5/imports/ui/components/text-input/styles.scss b/bigbluebutton-html5/imports/ui/components/text-input/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..5f13ce4fa070a5f077caf4c37f03d6de4b9472ce
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/text-input/styles.scss
@@ -0,0 +1,53 @@
+@import "/imports/ui/stylesheets/mixins/focus";
+@import "/imports/ui/stylesheets/mixins/_indicators";
+@import "/imports/ui/stylesheets/variables/_all";
+
+.wrapper {
+  display: flex;
+  flex-direction: row;
+}
+
+.textarea {
+  @include inputFocus(var(--color-blue-light));
+
+  flex: 1;
+  background: #fff;
+  background-clip: padding-box;
+  margin: 0;
+  color: var(--color-text);
+  -webkit-appearance: none;
+  padding: calc(var(--sm-padding-y) * 2.5) calc(var(--sm-padding-x) * 1.25);
+  resize: none;
+  transition: none;
+  border-radius: var(--border-radius);
+  font-size: var(--font-size-base);
+  min-height: 2.5rem;
+  max-height: 10rem;
+  border: 1px solid var(--color-gray-lighter);
+  box-shadow: 0 0 0 1px var(--color-gray-lighter);
+
+  &:hover {
+    @include highContrastOutline();
+  }
+
+  &:active,
+  &:focus {
+    @include highContrastOutline();
+    outline-style: solid;
+  }
+}
+
+.button {
+  margin:0 0 0 var(--sm-padding-x);
+  align-self: center;
+  font-size: 0.9rem;
+
+  [dir="rtl"]  & {
+    margin: 0 var(--sm-padding-x) 0 0;
+    -webkit-transform: scale(-1, 1);
+    -moz-transform: scale(-1, 1);
+    -ms-transform: scale(-1, 1);
+    -o-transform: scale(-1, 1);
+    transform: scale(-1, 1);
+  }
+}
diff --git a/bigbluebutton-html5/imports/ui/components/waiting-users/component.jsx b/bigbluebutton-html5/imports/ui/components/waiting-users/component.jsx
index 94d48900110473774acdf4a4ca377e2730ea85e1..db6ee3d12c3a5993bc5acaf0beda90a37a343bd0 100755
--- a/bigbluebutton-html5/imports/ui/components/waiting-users/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/waiting-users/component.jsx
@@ -4,6 +4,7 @@ import { Session } from 'meteor/session';
 import { defineMessages, injectIntl } from 'react-intl';
 import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
 import UserAvatar from '/imports/ui/components/user-avatar/component';
+import TextInput from '/imports/ui/components/text-input/component';
 import Button from '/imports/ui/components/button/component';
 import { styles } from './styles';
 
@@ -48,6 +49,14 @@ const intlMessages = defineMessages({
     id: 'app.userList.guest.rememberChoice',
     description: 'Remember label for checkbox',
   },
+  emptyMessage: {
+    id: 'app.userList.guest.emptyMessage',
+    description: 'Empty guest lobby message label',
+  },
+  inputPlaceholder: {
+    id: 'app.userList.guest.inputPlaceholder',
+    description: 'Placeholder to guest lobby message input',
+  },
   accept: {
     id: 'app.userList.guest.acceptLabel',
     description: 'Accept guest button label'
@@ -153,6 +162,9 @@ const WaitingUsers = (props) => {
     guestUsers,
     guestUsersCall,
     changeGuestPolicy,
+    isGuestLobbyMessageEnabled,
+    setGuestLobbyMessage,
+    guestLobbyMessage,
     authenticatedGuest,
   } = props;
 
@@ -234,6 +246,16 @@ const WaitingUsers = (props) => {
           />
         </div>
       </header>
+      {isGuestLobbyMessageEnabled ? (
+        <div className={styles.lobbyMessage}>
+          <TextInput
+            maxLength={128}
+            placeholder={intl.formatMessage(intlMessages.inputPlaceholder)}
+            send={setGuestLobbyMessage}
+          />
+          <p><i>"{guestLobbyMessage.length > 0 ? guestLobbyMessage : intl.formatMessage(intlMessages.emptyMessage)}"</i></p>
+        </div>
+      ) : null}
       <div>
         <div>
           <p className={styles.mainTitle}>{intl.formatMessage(intlMessages.optionTitle)}</p>
diff --git a/bigbluebutton-html5/imports/ui/components/waiting-users/container.jsx b/bigbluebutton-html5/imports/ui/components/waiting-users/container.jsx
index 7e6e879ae4038f9091894070e8b581c7fb7b2bee..0341da5a5daa35ba6496687121c5988b23fbb5fc 100644
--- a/bigbluebutton-html5/imports/ui/components/waiting-users/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/waiting-users/container.jsx
@@ -38,6 +38,9 @@ export default withTracker(() => {
     authenticatedUsers,
     guestUsersCall: Service.guestUsersCall,
     changeGuestPolicy: Service.changeGuestPolicy,
+    isGuestLobbyMessageEnabled: Service.isGuestLobbyMessageEnabled,
+    setGuestLobbyMessage: Service.setGuestLobbyMessage,
+    guestLobbyMessage: Service.getGuestLobbyMessage(),
     authenticatedGuest,
   };
 })(WaitingContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/waiting-users/service.js b/bigbluebutton-html5/imports/ui/components/waiting-users/service.js
index 23392f0c522fb66f18ee4c0e7cd1c78b1f3fdd4a..62d4165000c1d6912149c9a3f8f7310c14d79722 100644
--- a/bigbluebutton-html5/imports/ui/components/waiting-users/service.js
+++ b/bigbluebutton-html5/imports/ui/components/waiting-users/service.js
@@ -1,9 +1,30 @@
+import Meetings from '/imports/api/meetings';
+import Auth from '/imports/ui/services/auth';
 import { makeCall } from '/imports/ui/services/api';
 
 const guestUsersCall = (guestsArray, status) => makeCall('allowPendingUsers', guestsArray, status);
 
 const changeGuestPolicy = policyRule => makeCall('changeGuestPolicy', policyRule);
+
+const isGuestLobbyMessageEnabled = Meteor.settings.public.app.enableGuestLobbyMessage;
+
+const getGuestLobbyMessage = () => {
+  const meeting = Meetings.findOne(
+    { meetingId: Auth.meetingID },
+    { fields: { guestLobbyMessage: 1 } },
+  );
+
+  if (meeting) return meeting.guestLobbyMessage;
+
+  return '';
+};
+
+const setGuestLobbyMessage = (message) => makeCall('setGuestLobbyMessage', message);
+
 export default {
   guestUsersCall,
   changeGuestPolicy,
+  isGuestLobbyMessageEnabled,
+  getGuestLobbyMessage,
+  setGuestLobbyMessage,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/waiting-users/styles.scss b/bigbluebutton-html5/imports/ui/components/waiting-users/styles.scss
index 95f43ac21c3f6de01342ed9b48406460913312ac..bc5791c572a2aed78bbe4736c9678127d836f81a 100644
--- a/bigbluebutton-html5/imports/ui/components/waiting-users/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/waiting-users/styles.scss
@@ -186,6 +186,18 @@
   text-overflow: ellipsis;
 }
 
+.lobbyMessage {
+  border-bottom: 1px solid var(--color-gray-lightest);
+
+  p {
+    background-color: var(--color-off-white);
+    box-sizing: border-box;
+    color: var(--color-gray);
+    padding: 1rem;
+    text-align: center;
+  }
+}
+
 .rememberContainer {
   margin: 1rem 1rem;
   height: 2rem;
diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml
index 6d651d680c9d8135b19350b7f6ee08b6424c8a0f..908c64dad69c2fb6348a0d7c04e2fe000d9af81c 100755
--- a/bigbluebutton-html5/private/config/settings.yml
+++ b/bigbluebutton-html5/private/config/settings.yml
@@ -22,6 +22,7 @@ public:
     # in some cases we want only custom logoutUrl to be used when provided on meeting create. Default value: true
     allowDefaultLogoutUrl: true
     allowUserLookup: false
+    enableGuestLobbyMessage: true
     enableNetworkInformation: false
     enableLimitOfViewersInWebcam: false
     enableMultipleCameras: true
@@ -290,6 +291,7 @@ public:
   poll:
     enabled: true
     max_custom: 5
+    allowDragAndDropFile: false
   captions:
     enabled: true
     enableDictation: false
diff --git a/bigbluebutton-html5/private/static/guest-wait/guest-wait.html b/bigbluebutton-html5/private/static/guest-wait/guest-wait.html
index 8de097f3027098496106b94e30f25909d1f2511b..e2181188dbd3e66fe4e4901175bc28668dd5ff8b 100755
--- a/bigbluebutton-html5/private/static/guest-wait/guest-wait.html
+++ b/bigbluebutton-html5/private/static/guest-wait/guest-wait.html
@@ -59,12 +59,24 @@
       }
   }
   </style>
-  
+
   <script type="text/javascript">
     function updateMessage(message) {
       document.querySelector('#content > p').innerHTML = message;
     }
 
+    var lobbyMessage = '';
+    function updateLobbyMessage(message) {
+      if (message !== lobbyMessage) {
+        lobbyMessage = message;
+        if (lobbyMessage.length !== 0) {
+          updateMessage(lobbyMessage);
+        } else {
+          updateMessage('Please wait for a moderator to approve you joining the meeting.');
+        }
+      }
+    }
+
     function findSessionToken() {
       return location.search
         .substr(1)
@@ -75,7 +87,7 @@
     };
 
     function fetchGuestWait(sessionToken) {
-      const GUEST_WAIT_ENDPOINT = '/bigbluebutton/api/guestWait';      
+      const GUEST_WAIT_ENDPOINT = '/bigbluebutton/api/guestWait';
       const urlTest = new URL(`${window.location.origin}${GUEST_WAIT_ENDPOINT}`);
       const concatedParams = sessionToken.concat('&redirect=false');
       urlTest.search = concatedParams;
@@ -95,7 +107,6 @@
         fetchGuestWait(token)
         .then(async (resp) => await resp.json())
         .then((data) => {
-          console.log("data=" + JSON.stringify(data));
           var status = data.response.guestStatus;
 
           if (REDIRECT_STATUSES.includes(status)) {
@@ -104,6 +115,8 @@
             return;
           }
 
+          updateLobbyMessage(data.response.lobbyMessage);
+
           return pollGuestStatus(token, attempt + 1, limit, everyMs);
         });
       }, everyMs);
diff --git a/bigbluebutton-html5/private/locales/ar.json b/bigbluebutton-html5/public/locales/ar.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/ar.json
rename to bigbluebutton-html5/public/locales/ar.json
diff --git a/bigbluebutton-html5/private/locales/az.json b/bigbluebutton-html5/public/locales/az.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/az.json
rename to bigbluebutton-html5/public/locales/az.json
diff --git a/bigbluebutton-html5/private/locales/bg_BG.json b/bigbluebutton-html5/public/locales/bg_BG.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/bg_BG.json
rename to bigbluebutton-html5/public/locales/bg_BG.json
diff --git a/bigbluebutton-html5/private/locales/ca.json b/bigbluebutton-html5/public/locales/ca.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/ca.json
rename to bigbluebutton-html5/public/locales/ca.json
diff --git a/bigbluebutton-html5/private/locales/cs_CZ.json b/bigbluebutton-html5/public/locales/cs_CZ.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/cs_CZ.json
rename to bigbluebutton-html5/public/locales/cs_CZ.json
diff --git a/bigbluebutton-html5/private/locales/da.json b/bigbluebutton-html5/public/locales/da.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/da.json
rename to bigbluebutton-html5/public/locales/da.json
diff --git a/bigbluebutton-html5/private/locales/de.json b/bigbluebutton-html5/public/locales/de.json
similarity index 99%
rename from bigbluebutton-html5/private/locales/de.json
rename to bigbluebutton-html5/public/locales/de.json
index 53555c2f9daf9d8546add45088bbbbc0e4cf9455..a2776b6914f13f8c6182961753fa6f71c630d896 100644
--- a/bigbluebutton-html5/private/locales/de.json
+++ b/bigbluebutton-html5/public/locales/de.json
@@ -219,6 +219,10 @@
     "app.poll.hidePollDesc": "Versteckt das Umfragemenü",
     "app.poll.quickPollInstruction": "Wählen Sie eine der unten stehenden Optionen, um die Umfrage zu starten.",
     "app.poll.activePollInstruction": "Lassen Sie dieses Fenster offen, um auf die Antworten der Teilnehmer zu warten. Sobald Sie auf 'Umfrageergebnisse veröffentlichen' klicken, werden die Ergebnisse angezeigt und die Umfrage beendet.",
+    "app.poll.customPollLabel": "Benutzerdefinierte Umfrage",
+    "app.poll.startCustomLabel": "Benutzerdefinierte Umfrage starten",
+    "app.poll.dragDropPollInstruction": "Ziehen Sie per drag and Drop eine Textdatei mit den Umfrageoptionen auf das markierte Feld",
+    "app.poll.customPollTextArea": "Umfrageoptionen ausfüllen",
     "app.poll.publishLabel": "Umfrageergebnisse veröffentlichen",
     "app.poll.backLabel": "Umfrage starten",
     "app.poll.closeLabel": "Schließen",
diff --git a/bigbluebutton-html5/private/locales/el_GR.json b/bigbluebutton-html5/public/locales/el_GR.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/el_GR.json
rename to bigbluebutton-html5/public/locales/el_GR.json
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/public/locales/en.json
similarity index 99%
rename from bigbluebutton-html5/private/locales/en.json
rename to bigbluebutton-html5/public/locales/en.json
index 2ac4f36d93bb81a3bf1e39ff4f2efa15ae11b71b..142097d196a2d0b939fca46710e7d1da026d4c95 100755
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/public/locales/en.json
@@ -49,6 +49,7 @@
     "app.captions.pad.dictationStop": "Stop dictation",
     "app.captions.pad.dictationOnDesc": "Turns speech recognition on",
     "app.captions.pad.dictationOffDesc": "Turns speech recognition off",
+    "app.textInput.sendLabel": "Send",
     "app.note.title": "Shared Notes",
     "app.note.label": "Note",
     "app.note.hideNoteLabel": "Hide note",
@@ -134,6 +135,7 @@
     "app.meeting.ended": "This session has ended",
     "app.meeting.meetingTimeRemaining": "Meeting time remaining: {0}",
     "app.meeting.meetingTimeHasEnded": "Time ended. Meeting will close soon",
+    "app.meeting.endedByUserMessage": "The meeting was ended by {0}",
     "app.meeting.endedMessage": "You will be forwarded back to the home screen",
     "app.meeting.alertMeetingEndsUnderMinutesSingular": "Meeting is closing in one minute.",
     "app.meeting.alertMeetingEndsUnderMinutesPlural": "Meeting is closing in {0} minutes.",
@@ -220,6 +222,8 @@
     "app.poll.hidePollDesc": "Hides the poll menu pane",
     "app.poll.quickPollInstruction": "Select an option below to start your poll.",
     "app.poll.activePollInstruction": "Leave this panel open to see live responses to your poll. When you are ready, select 'Publish polling results' to publish the results and end the poll.",
+    "app.poll.dragDropPollInstruction": "To fill the poll values, drag a text file with the poll values onto the highlighted field",
+    "app.poll.customPollTextArea": "Fill poll values",
     "app.poll.publishLabel": "Publish polling results",
     "app.poll.backLabel": "Start A Poll",
     "app.poll.closeLabel": "Close",
@@ -539,6 +543,8 @@
     "app.userList.guest.pendingGuestUsers": "{0} Pending Guest Users",
     "app.userList.guest.pendingGuestAlert": "Has joined the session and is waiting for your approval.",
     "app.userList.guest.rememberChoice": "Remember choice",
+    "app.userList.guest.emptyMessage": "There is currently no message",
+    "app.userList.guest.inputPlaceholder": "Message to the guests' lobby",
     "app.userList.guest.acceptLabel": "Accept",
     "app.userList.guest.denyLabel": "Deny",
     "app.user-info.title": "Directory Lookup",
diff --git a/bigbluebutton-html5/private/locales/eo.json b/bigbluebutton-html5/public/locales/eo.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/eo.json
rename to bigbluebutton-html5/public/locales/eo.json
diff --git a/bigbluebutton-html5/private/locales/es.json b/bigbluebutton-html5/public/locales/es.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/es.json
rename to bigbluebutton-html5/public/locales/es.json
diff --git a/bigbluebutton-html5/private/locales/es_ES.json b/bigbluebutton-html5/public/locales/es_ES.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/es_ES.json
rename to bigbluebutton-html5/public/locales/es_ES.json
diff --git a/bigbluebutton-html5/private/locales/es_MX.json b/bigbluebutton-html5/public/locales/es_MX.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/es_MX.json
rename to bigbluebutton-html5/public/locales/es_MX.json
diff --git a/bigbluebutton-html5/private/locales/et.json b/bigbluebutton-html5/public/locales/et.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/et.json
rename to bigbluebutton-html5/public/locales/et.json
diff --git a/bigbluebutton-html5/private/locales/eu.json b/bigbluebutton-html5/public/locales/eu.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/eu.json
rename to bigbluebutton-html5/public/locales/eu.json
diff --git a/bigbluebutton-html5/private/locales/fa_IR.json b/bigbluebutton-html5/public/locales/fa_IR.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/fa_IR.json
rename to bigbluebutton-html5/public/locales/fa_IR.json
diff --git a/bigbluebutton-html5/private/locales/fi.json b/bigbluebutton-html5/public/locales/fi.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/fi.json
rename to bigbluebutton-html5/public/locales/fi.json
diff --git a/bigbluebutton-html5/private/locales/fr.json b/bigbluebutton-html5/public/locales/fr.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/fr.json
rename to bigbluebutton-html5/public/locales/fr.json
diff --git a/bigbluebutton-html5/private/locales/gl.json b/bigbluebutton-html5/public/locales/gl.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/gl.json
rename to bigbluebutton-html5/public/locales/gl.json
diff --git a/bigbluebutton-html5/private/locales/he.json b/bigbluebutton-html5/public/locales/he.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/he.json
rename to bigbluebutton-html5/public/locales/he.json
diff --git a/bigbluebutton-html5/private/locales/hi_IN.json b/bigbluebutton-html5/public/locales/hi_IN.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/hi_IN.json
rename to bigbluebutton-html5/public/locales/hi_IN.json
diff --git a/bigbluebutton-html5/private/locales/hr.json b/bigbluebutton-html5/public/locales/hr.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/hr.json
rename to bigbluebutton-html5/public/locales/hr.json
diff --git a/bigbluebutton-html5/private/locales/hu_HU.json b/bigbluebutton-html5/public/locales/hu_HU.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/hu_HU.json
rename to bigbluebutton-html5/public/locales/hu_HU.json
diff --git a/bigbluebutton-html5/private/locales/hy.json b/bigbluebutton-html5/public/locales/hy.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/hy.json
rename to bigbluebutton-html5/public/locales/hy.json
diff --git a/bigbluebutton-html5/private/locales/id.json b/bigbluebutton-html5/public/locales/id.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/id.json
rename to bigbluebutton-html5/public/locales/id.json
diff --git a/bigbluebutton-html5/private/locales/it_IT.json b/bigbluebutton-html5/public/locales/it_IT.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/it_IT.json
rename to bigbluebutton-html5/public/locales/it_IT.json
diff --git a/bigbluebutton-html5/private/locales/ja.json b/bigbluebutton-html5/public/locales/ja.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/ja.json
rename to bigbluebutton-html5/public/locales/ja.json
diff --git a/bigbluebutton-html5/private/locales/ka.json b/bigbluebutton-html5/public/locales/ka.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/ka.json
rename to bigbluebutton-html5/public/locales/ka.json
diff --git a/bigbluebutton-html5/private/locales/kk.json b/bigbluebutton-html5/public/locales/kk.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/kk.json
rename to bigbluebutton-html5/public/locales/kk.json
diff --git a/bigbluebutton-html5/private/locales/km.json b/bigbluebutton-html5/public/locales/km.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/km.json
rename to bigbluebutton-html5/public/locales/km.json
diff --git a/bigbluebutton-html5/private/locales/kn.json b/bigbluebutton-html5/public/locales/kn.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/kn.json
rename to bigbluebutton-html5/public/locales/kn.json
diff --git a/bigbluebutton-html5/private/locales/ko_KR.json b/bigbluebutton-html5/public/locales/ko_KR.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/ko_KR.json
rename to bigbluebutton-html5/public/locales/ko_KR.json
diff --git a/bigbluebutton-html5/private/locales/lo_LA.json b/bigbluebutton-html5/public/locales/lo_LA.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/lo_LA.json
rename to bigbluebutton-html5/public/locales/lo_LA.json
diff --git a/bigbluebutton-html5/private/locales/lt_LT.json b/bigbluebutton-html5/public/locales/lt_LT.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/lt_LT.json
rename to bigbluebutton-html5/public/locales/lt_LT.json
diff --git a/bigbluebutton-html5/private/locales/lv.json b/bigbluebutton-html5/public/locales/lv.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/lv.json
rename to bigbluebutton-html5/public/locales/lv.json
diff --git a/bigbluebutton-html5/private/locales/mn_MN.json b/bigbluebutton-html5/public/locales/mn_MN.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/mn_MN.json
rename to bigbluebutton-html5/public/locales/mn_MN.json
diff --git a/bigbluebutton-html5/private/locales/nb_NO.json b/bigbluebutton-html5/public/locales/nb_NO.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/nb_NO.json
rename to bigbluebutton-html5/public/locales/nb_NO.json
diff --git a/bigbluebutton-html5/private/locales/nl.json b/bigbluebutton-html5/public/locales/nl.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/nl.json
rename to bigbluebutton-html5/public/locales/nl.json
diff --git a/bigbluebutton-html5/private/locales/oc.json b/bigbluebutton-html5/public/locales/oc.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/oc.json
rename to bigbluebutton-html5/public/locales/oc.json
diff --git a/bigbluebutton-html5/private/locales/pl_PL.json b/bigbluebutton-html5/public/locales/pl_PL.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/pl_PL.json
rename to bigbluebutton-html5/public/locales/pl_PL.json
diff --git a/bigbluebutton-html5/private/locales/pt.json b/bigbluebutton-html5/public/locales/pt.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/pt.json
rename to bigbluebutton-html5/public/locales/pt.json
diff --git a/bigbluebutton-html5/private/locales/pt_BR.json b/bigbluebutton-html5/public/locales/pt_BR.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/pt_BR.json
rename to bigbluebutton-html5/public/locales/pt_BR.json
diff --git a/bigbluebutton-html5/private/locales/ro_RO.json b/bigbluebutton-html5/public/locales/ro_RO.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/ro_RO.json
rename to bigbluebutton-html5/public/locales/ro_RO.json
diff --git a/bigbluebutton-html5/private/locales/ru.json b/bigbluebutton-html5/public/locales/ru.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/ru.json
rename to bigbluebutton-html5/public/locales/ru.json
diff --git a/bigbluebutton-html5/private/locales/ru_RU.json b/bigbluebutton-html5/public/locales/ru_RU.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/ru_RU.json
rename to bigbluebutton-html5/public/locales/ru_RU.json
diff --git a/bigbluebutton-html5/private/locales/sk_SK.json b/bigbluebutton-html5/public/locales/sk_SK.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/sk_SK.json
rename to bigbluebutton-html5/public/locales/sk_SK.json
diff --git a/bigbluebutton-html5/private/locales/sl.json b/bigbluebutton-html5/public/locales/sl.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/sl.json
rename to bigbluebutton-html5/public/locales/sl.json
diff --git a/bigbluebutton-html5/private/locales/sr.json b/bigbluebutton-html5/public/locales/sr.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/sr.json
rename to bigbluebutton-html5/public/locales/sr.json
diff --git a/bigbluebutton-html5/private/locales/sv_SE.json b/bigbluebutton-html5/public/locales/sv_SE.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/sv_SE.json
rename to bigbluebutton-html5/public/locales/sv_SE.json
diff --git a/bigbluebutton-html5/private/locales/te.json b/bigbluebutton-html5/public/locales/te.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/te.json
rename to bigbluebutton-html5/public/locales/te.json
diff --git a/bigbluebutton-html5/private/locales/th.json b/bigbluebutton-html5/public/locales/th.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/th.json
rename to bigbluebutton-html5/public/locales/th.json
diff --git a/bigbluebutton-html5/private/locales/th_TH.json b/bigbluebutton-html5/public/locales/th_TH.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/th_TH.json
rename to bigbluebutton-html5/public/locales/th_TH.json
diff --git a/bigbluebutton-html5/private/locales/tr.json b/bigbluebutton-html5/public/locales/tr.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/tr.json
rename to bigbluebutton-html5/public/locales/tr.json
diff --git a/bigbluebutton-html5/private/locales/tr_TR.json b/bigbluebutton-html5/public/locales/tr_TR.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/tr_TR.json
rename to bigbluebutton-html5/public/locales/tr_TR.json
diff --git a/bigbluebutton-html5/private/locales/uk_UA.json b/bigbluebutton-html5/public/locales/uk_UA.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/uk_UA.json
rename to bigbluebutton-html5/public/locales/uk_UA.json
diff --git a/bigbluebutton-html5/private/locales/vi.json b/bigbluebutton-html5/public/locales/vi.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/vi.json
rename to bigbluebutton-html5/public/locales/vi.json
diff --git a/bigbluebutton-html5/private/locales/vi_VN.json b/bigbluebutton-html5/public/locales/vi_VN.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/vi_VN.json
rename to bigbluebutton-html5/public/locales/vi_VN.json
diff --git a/bigbluebutton-html5/private/locales/zh_CN.json b/bigbluebutton-html5/public/locales/zh_CN.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/zh_CN.json
rename to bigbluebutton-html5/public/locales/zh_CN.json
diff --git a/bigbluebutton-html5/private/locales/zh_TW.json b/bigbluebutton-html5/public/locales/zh_TW.json
similarity index 100%
rename from bigbluebutton-html5/private/locales/zh_TW.json
rename to bigbluebutton-html5/public/locales/zh_TW.json
diff --git a/bigbluebutton-html5/transifex.sh b/bigbluebutton-html5/transifex.sh
index de1efe3eaa73f91ca62b80f75b450bf59da70587..b11dc50f7ac3f69058c0106abd1dfa425230b819 100755
--- a/bigbluebutton-html5/transifex.sh
+++ b/bigbluebutton-html5/transifex.sh
@@ -5,6 +5,14 @@ RED='\033[0;31m'
 GREEN='\033[1;32m'
 NC='\033[0m'
 SOURCE_LANGUAGE="en"
+LOCALES_DIRECTORY="./public/locales"
+PULL_SOURCE=false
+
+if [[ ! -e $LOCALES_DIRECTORY ]]; then
+    echo -e "Directory ${RED}$LOCALES_DIRECTORY${NC} does not exist, creating"
+    mkdir $LOCALES_DIRECTORY
+    PULL_SOURCE=true
+fi
 
 if [ "$#" = 0 ]
 then
@@ -33,19 +41,19 @@ else
           echo "$AVAILABLE_TRANSLATIONS" | while read l
             do
               LOCALE=$( echo "$l" | tr -d '[:space:]' )
-              if [ "$LOCALE" == "$SOURCE_LANGUAGE" ]; then
-                continue # do not pull the source file
+              if [ "$LOCALE" == "$SOURCE_LANGUAGE" ] && [ "$PULL_SOURCE" == false ]; then
+                continue # only pull source file if locales folder did not exist
               fi
               TRANSLATION=$(curl -L --user "$USER":"$PW" -X GET "https://www.transifex.com/api/2/project/bigbluebutton-v23-html5-client/resource/enjson/translation/$LOCALE/?mode=onlytranslated&file")
               NO_EMPTY_STRINGS=$(echo "$TRANSLATION" | sed '/: *\"\"/D' | sed '/}$/D')
-              if [ $(echo "$NO_EMPTY_STRINGS" | wc -l) == 1 ]
+              if [ $(echo "$NO_EMPTY_STRINGS" | wc -l) -lt 100 ]
               then
-                echo -e "${RED}WARN:${NC} translation file $LOCALE.json is empty\n${RED}WARN:${NC} $LOCALE.json not created"
+                echo -e "${RED}WARN:${NC} translation file $LOCALE.json contains less than 100 lines\n${RED}WARN:${NC} $LOCALE.json not created"
                 continue
               else
                 NO_TRAILING_COMMA=$(echo "$NO_EMPTY_STRINGS" | sed  '$ s/,$//')
-                echo "$NO_TRAILING_COMMA" > ./private/locales/"$LOCALE".json
-                echo -e "\n}\n" >> ./private/locales/"$LOCALE".json
+                echo "$NO_TRAILING_COMMA" > "$LOCALES_DIRECTORY/$LOCALE".json
+                echo -e "\n}\n" >> "$LOCALES_DIRECTORY/$LOCALE".json
                 echo -e "Added translation file $LOCALE.json : ${GREEN}✓${NC}"
               fi
             done
@@ -56,13 +64,13 @@ else
             echo -e "${RED}Err${NC}: Translations not found for locale ->${RED}$ARG${NC}<-"
           else
             NO_EMPTY_STRINGS=$(echo "$TRANSLATION" | sed '/: *\"\"/D' | sed '/}$/D')
-            if [ $(echo "$NO_EMPTY_STRINGS" | wc -l) == 1 ]
+            if [ $(echo "$NO_EMPTY_STRINGS" | wc -l) -lt 100 ]
             then
-              echo -e "${RED}WARN:${NC} translation file $ARG.json is empty\n${RED}WARN:${NC} $ARG.json not created"
+              echo -e "${RED}WARN:${NC} translation file $ARG.json contains less than 100 lines\n${RED}WARN:${NC} $ARG.json not created"
             else
               NO_TRAILING_COMMA=$(echo "$NO_EMPTY_STRINGS" | sed  '$ s/,//')
-              echo "$NO_TRAILING_COMMA" > ./private/locales/"$ARG".json
-              echo -e "\n}\n" >> ./private/locales/"$ARG".json
+              echo "$NO_TRAILING_COMMA" > "$LOCALES_DIRECTORY/$ARG".json
+              echo -e "\n}\n" >> "$LOCALES_DIRECTORY/$ARG".json
               echo -e "Added translation file $ARG.json :${GREEN} ✓${NC}"
             fi
           fi
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 9e4fd1028f418bac486f7064fd2571033cf68235..77049971a325d2109d268c2ad0ce4cb289056fae 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
@@ -1323,6 +1323,7 @@ class ApiController {
       // Get the client url we stored in the join api call before
       // being told to wait.
       String clientURL = us.clientUrl;
+      String lobbyMsg = meeting.getGuestLobbyMessage()
       log.info("clientURL = " + clientURL)
       log.info("redirect = ." + redirectClient)
       if (!StringUtils.isEmpty(params.redirect)) {
@@ -1412,6 +1413,7 @@ class ApiController {
               auth_token us.authToken
               session_token session[sessionToken]
               guestStatus guestWaitStatus
+              lobbyMessage lobbyMsg
               url destUrl
             }
             render(contentType: "application/json", text: builder.toPrettyString())