diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index f44632402914b6169cc220ff9bd6221f0188021e..9547614bf47c41fb29c3d141c4b9063f38fc87d7 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -21,6 +21,7 @@ HOW TO WRITE A GOOD PULL REQUEST?
 
 ### Closes Issue(s)
 
+closes #...
 <!-- List here all the issues closed by this pull request. Use keyword `closes` before each issue number -->
 
 ### Motivation
diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx
index 602ba348d78f00ee250aee4b5b5a485237673764..5ee1d8e6e4c93356a5993eb30de52107f5ddfde7 100755
--- a/bigbluebutton-html5/imports/startup/client/base.jsx
+++ b/bigbluebutton-html5/imports/startup/client/base.jsx
@@ -20,7 +20,6 @@ import deviceInfo from '/imports/utils/deviceInfo';
 import getFromUserSettings from '/imports/ui/services/users-settings';
 import LayoutManager from '/imports/ui/components/layout/layout-manager';
 import { withLayoutContext } from '/imports/ui/components/layout/context';
-import IntlStartup from '/imports/startup/client/intl';
 import VideoService from '/imports/ui/components/video-provider/service';
 
 const CHAT_CONFIG = Meteor.settings.public.chat;
@@ -185,6 +184,7 @@ class Base extends Component {
     const {
       codeError,
       ejected,
+      ejectedReason,
       meetingExist,
       meetingHasEnded,
       meetingIsBreakout,
@@ -197,27 +197,26 @@ class Base extends Component {
     }
 
     if (ejected) {
-      return (<MeetingEnded code="403" />);
+      return (<MeetingEnded code="403" reason={ejectedReason} />);
     }
 
-    if ((meetingHasEnded || User.loggedOut) && meetingIsBreakout) {
+    if ((meetingHasEnded || User?.loggedOut) && meetingIsBreakout) {
       window.close();
       return null;
     }
 
-    if (((meetingHasEnded && !meetingIsBreakout)) || (codeError && (User && User.loggedOut))) {
+    if (((meetingHasEnded && !meetingIsBreakout)) || (codeError && User?.loggedOut)) {
       return (<MeetingEnded code={codeError} />);
     }
 
     if (codeError && !meetingHasEnded) {
       // 680 is set for the codeError when the user requests a logout
       if (codeError !== '680') {
-        logger.error({ logCode: 'startup_client_usercouldnotlogin_error' }, `User could not log in HTML5, hit ${codeError}`);
         return (<ErrorScreen code={codeError} />);
       }
       return (<MeetingEnded code={codeError} />);
     }
-    // this.props.annotationsHandler.stop();
+
     return (<AppContainer {...this.props} baseControls={stateControls} />);
   }
 
@@ -264,6 +263,7 @@ const BaseContainer = withTracker(() => {
     approved: 1,
     authed: 1,
     ejected: 1,
+    ejectedReason: 1,
     color: 1,
     effectiveConnectionType: 1,
     extId: 1,
@@ -273,6 +273,8 @@ const BaseContainer = withTracker(() => {
     loggedOut: 1,
     meetingId: 1,
     userId: 1,
+    inactivityCheck: 1,
+    responseDelay: 1,
   };
   const User = Users.findOne({ intId: credentials.requesterUserId }, { fields });
   const meeting = Meetings.findOne({ meetingId }, {
@@ -286,8 +288,10 @@ const BaseContainer = withTracker(() => {
     Session.set('codeError', '410');
   }
 
-  const approved = User && User.approved && User.guest;
-  const ejected = User && User.ejected;
+  const approved = User?.approved && User?.guest;
+  const ejected = User?.ejected;
+  const ejectedReason = User?.ejectedReason;
+
   let userSubscriptionHandler;
 
   Breakouts.find({}, { fields: { _id: 1 } }).observeChanges({
@@ -393,6 +397,7 @@ const BaseContainer = withTracker(() => {
   return {
     approved,
     ejected,
+    ejectedReason,
     userSubscriptionHandler,
     breakoutRoomSubscriptionHandler,
     meetingModeratorSubscriptionHandler,
diff --git a/bigbluebutton-html5/imports/ui/components/activity-check/component.jsx b/bigbluebutton-html5/imports/ui/components/activity-check/component.jsx
index 628a2e717a6b35050ce5bbddefac51d1ebd94748..8238296020f5ae2794ae95162a17b6709dd69f88 100644
--- a/bigbluebutton-html5/imports/ui/components/activity-check/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/activity-check/component.jsx
@@ -64,6 +64,7 @@ class ActivityCheck extends Component {
 
     return setInterval(() => {
       const remainingTime = responseDelay - 1;
+
       this.setState({
         responseDelay: remainingTime,
       });
@@ -96,6 +97,7 @@ class ActivityCheck extends Component {
           <p>{intl.formatMessage(intlMessages.activityCheckLabel, { 0: responseDelay })}</p>
           <Button
             color="primary"
+            disabled={responseDelay <= 0}
             label={intl.formatMessage(intlMessages.activityCheckButton)}
             onClick={handleInactivityDismiss}
             role="button"
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 73d65f226dac6720242a7032ad7696ef294fd4e5..c3188b8776082ce0be5581717835d84917455b82 100755
--- a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx
@@ -71,7 +71,9 @@ class ChatDropdown extends PureComponent {
   }
 
   getAvailableActions() {
-    const { intl, isMeteorConnected, amIModerator } = this.props;
+    const {
+      intl, isMeteorConnected, amIModerator, meetingName,
+    } = this.props;
 
     const clearIcon = 'delete';
     const saveIcon = 'download';
@@ -86,8 +88,11 @@ class ChatDropdown extends PureComponent {
         onClick={() => {
           const link = document.createElement('a');
           const mimeType = 'text/plain';
+          const date = new Date();
+          const time = `${date.getHours()}-${date.getMinutes()}`;
+          const dateString = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}_${time}`;
 
-          link.setAttribute('download', `public-chat-${Date.now()}.txt`);
+          link.setAttribute('download', `bbb-${meetingName}[public-chat]_${dateString}.txt`);
           link.setAttribute(
             'href',
             `data: ${mimeType} ;charset=utf-8,
diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/container.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..f00a5d6cc173a9276a4a19c549c70381b6af874c
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/container.jsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import { withTracker } from 'meteor/react-meteor-data';
+import Auth from '/imports/ui/services/auth';
+import Meetings from '/imports/api/meetings';
+import ChatDropdown from './component';
+
+const ChatDropdownContainer = ({ ...props }) => <ChatDropdown {...props} />;
+
+export default withTracker(() => {
+  const getMeetingName = () => {
+    const m = Meetings.findOne({ meetingId: Auth.meetingID },
+      { fields: { 'meetingProp.name': 1 } });
+    return m.meetingProp.name;
+  };
+
+  return {
+    meetingName: getMeetingName(),
+  };
+})(ChatDropdownContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/chat/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/component.jsx
index 44a2f0c6c66909301972225434f652e8e01bd940..05654954fbaf54b4c144640963d67ce7f91d5968 100755
--- a/bigbluebutton-html5/imports/ui/components/chat/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/component.jsx
@@ -8,7 +8,7 @@ import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
 import { styles } from './styles.scss';
 import MessageForm from './message-form/container';
 import MessageList from './message-list/container';
-import ChatDropdown from './chat-dropdown/component';
+import ChatDropdownContainer from './chat-dropdown/container';
 
 const ELEMENT_ID = 'chat-messages';
 
@@ -89,7 +89,12 @@ const Chat = (props) => {
                 accessKey={CLOSE_CHAT_AK}
               />
             )
-            : <ChatDropdown isMeteorConnected={isMeteorConnected} amIModerator={amIModerator} />
+            : (
+              <ChatDropdownContainer
+                isMeteorConnected={isMeteorConnected}
+                amIModerator={amIModerator}
+              />
+            )
         }
       </header>
       <MessageList
diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/component.jsx
index 4e9ebea3bfef182ca16827ef97796a5081ff4b3f..cade8f4c4b28b537fc7b03550fd4b47888ab8734 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/component.jsx
@@ -4,6 +4,7 @@ import { FormattedTime, defineMessages, injectIntl } from 'react-intl';
 import _ from 'lodash';
 import Icon from '/imports/ui/components/icon/component';
 import UserAvatar from '/imports/ui/components/user-avatar/component';
+import cx from 'classnames';
 import Message from './message/component';
 
 import { styles } from './styles';
@@ -42,10 +43,6 @@ const intlMessages = defineMessages({
     id: 'app.chat.pollResult',
     description: 'used in place of user name who published poll to chat',
   },
-  legendTitle: {
-    id: 'app.polling.pollingTitle',
-    description: 'heading for chat poll legend',
-  },
 });
 
 class MessageListItem extends Component {
@@ -111,13 +108,14 @@ class MessageListItem extends Component {
       handleReadMessage,
       scrollArea,
       intl,
-      chats,
+      messages,
     } = this.props;
 
-    if (chats.length < 1) return null;
+    if (messages && messages[0].text.includes('bbb-published-poll-<br/>')) {
+      return this.renderPollItem();
+    }
 
     const dateTime = new Date(time);
-
     const regEx = /<a[^>]+>/i;
 
     return (
@@ -149,7 +147,7 @@ class MessageListItem extends Component {
               </time>
             </div>
             <div className={styles.messages} data-test="chatUserMessage">
-              {chats.map(message => (
+              {messages.map(message => (
                 <Message
                   className={(regEx.test(message.text) ? styles.hyperlink : styles.message)}
                   key={message.id}
@@ -173,53 +171,17 @@ class MessageListItem extends Component {
       user,
       time,
       intl,
-      polls,
       isDefaultPoll,
+      messages,
+      scrollArea,
+      chatAreaId,
+      lastReadMessageTime,
+      handleReadMessage,
     } = this.props;
 
-    if (polls.length < 1) return null;
-
     const dateTime = new Date(time);
 
-    let pollText = [];
-    const pollElement = [];
-    const legendElements = [
-      (<div
-        className={styles.optionsTitle}
-        key={_.uniqueId('chat-poll-options-')}
-      >
-        {intl.formatMessage(intlMessages.legendTitle)}
-      </div>),
-    ];
-
-    let isDefault = true;
-    polls.forEach((poll) => {
-      isDefault = isDefaultPoll(poll.text);
-      pollText = poll.text.split('<br/>');
-      pollElement.push(pollText.map((p, index) => {
-        if (!isDefault) {
-          legendElements.push(
-            <div key={_.uniqueId('chat-poll-legend-')} className={styles.pollLegend}>
-              <span>{`${index + 1}: `}</span>
-              <span className={styles.pollOption}>{p.split(':')[0]}</span>
-            </div>,
-          );
-        }
-
-        return (
-          <div key={_.uniqueId('chat-poll-result-')} className={styles.pollLine}>
-            {!isDefault ? p.replace(p.split(':')[0], index + 1) : p}
-          </div>
-        );
-      }));
-    });
-
-    if (!isDefault) {
-      pollElement.push(<div key={_.uniqueId('chat-poll-separator-')} className={styles.divider} />);
-      pollElement.push(legendElements);
-    }
-
-    return polls ? (
+    return messages ? (
       <div className={styles.item} key={_.uniqueId('message-poll-item-')}>
         <div className={styles.wrapper} ref={(ref) => { this.item = ref; }}>
           <div className={styles.avatarWrapper}>
@@ -240,15 +202,19 @@ class MessageListItem extends Component {
                 <FormattedTime value={dateTime} />
               </time>
             </div>
-            <div className={styles.messages}>
-              {polls[0] ? (
-                <div className={styles.pollWrapper} style={{ borderLeft: `3px ${user.color} solid` }}>
-                  {
-                  pollElement
-                }
-                </div>
-              ) : null}
-            </div>
+            <Message
+              type="poll"
+              className={cx(styles.message, styles.pollWrapper)}
+              key={messages[0].id}
+              text={messages[0].text}
+              time={messages[0].time}
+              chatAreaId={chatAreaId}
+              lastReadMessageTime={lastReadMessageTime}
+              handleReadMessage={handleReadMessage}
+              scrollArea={scrollArea}
+              color={user.color}
+              isDefaultPoll={isDefaultPoll(messages[0].text.replace('bbb-published-poll-<br/>', ''))}
+            />
           </div>
         </div>
       </div>
@@ -266,10 +232,7 @@ class MessageListItem extends Component {
 
     return (
       <div className={styles.item}>
-        {[
-          this.renderPollItem(),
-          this.renderMessageItem(),
-        ]}
+        {this.renderMessageItem()}
       </div>
     );
   }
diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/container.jsx
index 5c7d7e0a476776ecaa42ebaea97dfd9c5f25a3f7..31b407d453f56cb6fe98a48f4cbd0c49b4f97bcb 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/container.jsx
@@ -15,26 +15,10 @@ export default withTracker(({ message }) => {
   const mappedMessage = ChatService.mapGroupMessage(message);
   const messages = mappedMessage.content;
 
-  const chats = [];
-  const polls = [];
-
-  if (messages.length > 0) {
-    messages.forEach((m) => {
-      if (m.text.includes('bbb-published-poll-<br/>')) {
-        m.text = m.text.replace(/^bbb-published-poll-<br\/>/g, '');
-        polls.push(m);
-      } else {
-        chats.push(m);
-      }
-    });
-  }
-
   return {
     messages,
     user: mappedMessage.sender,
     time: mappedMessage.time,
-    chats,
-    polls,
     isDefaultPoll: (pollText) => {
       const pollValue = pollText.replace(/<br\/>|[ :|%\n\d+]/g, '');
       switch (pollValue) {
diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/message/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/message/component.jsx
index e015975f84c037b8d95cc03d942a3f12865d1292..d7d7ade04ff122e76cacc0b88d9ab8b76ceb043c 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/message/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/message/component.jsx
@@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
 import PropTypes from 'prop-types';
 import _ from 'lodash';
 import fastdom from 'fastdom';
+import { defineMessages, injectIntl } from 'react-intl';
 
 const propTypes = {
   text: PropTypes.string.isRequired,
@@ -34,13 +35,22 @@ const isElementInViewport = (el) => {
   );
 };
 
-export default class MessageListItem extends PureComponent {
+const intlMessages = defineMessages({
+  legendTitle: {
+    id: 'app.polling.pollingTitle',
+    description: 'heading for chat poll legend',
+  },
+});
+
+class MessageListItem extends PureComponent {
   constructor(props) {
     super(props);
 
     this.ticking = false;
 
     this.handleMessageInViewport = _.debounce(this.handleMessageInViewport.bind(this), 50);
+
+    this.renderPollListItem = this.renderPollListItem.bind(this);
   }
 
   componentDidMount() {
@@ -145,17 +155,56 @@ export default class MessageListItem extends PureComponent {
     });
   }
 
+  renderPollListItem() {
+    const {
+      intl,
+      text,
+      className,
+      color,
+      isDefaultPoll,
+    } = this.props;
+
+    const formatBoldBlack = s => s.bold().fontcolor('black');
+
+    let _text = text.replace('bbb-published-poll-<br/>', '');
+
+    if (!isDefaultPoll) {
+      const entries = _text.split('<br/>');
+      const options = [];
+      entries.map((e) => { options.push([e.slice(0, e.indexOf(':'))]); return e; });
+      options.map((o, idx) => {
+        _text = formatBoldBlack(_text.replace(o, idx + 1));
+        return _text;
+      });
+      _text += formatBoldBlack(`<br/><br/>${intl.formatMessage(intlMessages.legendTitle)}`);
+      options.map((o, idx) => { _text += `<br/>${idx + 1}: ${o}`; return _text; });
+    }
+
+    return (
+      <p
+        className={className}
+        style={{ borderLeft: `3px ${color} solid` }}
+        ref={(ref) => { this.text = ref; }}
+        dangerouslySetInnerHTML={{ __html: isDefaultPoll ? formatBoldBlack(_text) : _text }}
+        data-test="chatMessageText"
+      />
+    );
+  }
+
   render() {
     const {
       text,
+      type,
       className,
     } = this.props;
 
+    if (type === 'poll') return this.renderPollListItem();
+
     return (
       <p
+        className={className}
         ref={(ref) => { this.text = ref; }}
         dangerouslySetInnerHTML={{ __html: text }}
-        className={className}
         data-test="chatMessageText"
       />
     );
@@ -164,3 +213,5 @@ export default class MessageListItem extends PureComponent {
 
 MessageListItem.propTypes = propTypes;
 MessageListItem.defaultProps = defaultProps;
+
+export default injectIntl(MessageListItem);
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 5b8817b1c6822a1662591941eb2c25846fa98abb..b4447341f5ec5b6b9c12b54d3898dc324ca5b3ee 100755
--- 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
@@ -4,7 +4,7 @@
   --systemMessage-background-color: #F9FBFC;
   --systemMessage-border-color: #C5CDD4;
   --systemMessage-font-color: var(--color-dark-grey);
-  --chat-poll-margin-sm: .25rem;
+  --chat-poll-margin-sm: .5rem;
 }
 
 .item {
@@ -159,42 +159,11 @@
   bottom: var(--border-size-large);
 }
 
-.pollLine {
-  overflow-wrap: break-word;
-  font-size: var(--font-size-large);
-  font-weight: 600;
-}
-
 .pollWrapper {
+  background: var(--systemMessage-background-color);
   border: solid 1px var(--color-gray-lighter);
   border-radius: var(--border-radius);
   padding: var(--chat-poll-margin-sm);
   padding-left: 1rem;
-  margin-top: .5rem;
-  background: var(--systemMessage-background-color);
-}
-
-.pollLegend {
-  display: flex;
-  flex-direction: row;
-  margin-top: var(--chat-poll-margin-sm);
-}
-
-.pollOption {
-  word-break: break-word;
-  margin-left: var(--md-padding-y);
-}
-
-.optionsTitle {
-  font-weight: bold;
-  margin-top: var(--md-padding-y);
-}
-
-.divider {
-  position: relative;
-  height: 1px;
-  width: 95%;
-  margin-right: auto;
-  margin-top: var(--md-padding-y);
-  border-bottom: solid 1px var(--color-gray-lightest);
+  margin-top: var(--chat-poll-margin-sm) !important;
 }
diff --git a/bigbluebutton-html5/imports/ui/components/chat/service.js b/bigbluebutton-html5/imports/ui/components/chat/service.js
index 5579ad7ce4ab1d7612b3dda360ea71d668efcf58..ce745e1966ab64e1568ab98340cf2add88433d25 100755
--- a/bigbluebutton-html5/imports/ui/components/chat/service.js
+++ b/bigbluebutton-html5/imports/ui/components/chat/service.js
@@ -31,6 +31,8 @@ const UnsentMessagesCollection = new Mongo.Collection(null);
 // session for closed chat list
 const CLOSED_CHAT_LIST_KEY = 'closedChatList';
 
+const POLL_MESSAGE_PREFIX = 'bbb-published-poll-<br/>';
+
 const getUser = userId => Users.findOne({ userId });
 
 const getWelcomeProp = () => Meetings.findOne({ meetingId: Auth.meetingID },
@@ -83,11 +85,15 @@ const reduceGroupMessages = (previous, current) => {
     return previous.concat(currentMessage);
   }
   // Check if the last message is from the same user and time discrepancy
-  // between the two messages exceeds window and then group current message
-  // with the last one
+  // between the two messages exceeds window and then group current
+  // message with the last one
   const timeOfLastMessage = lastMessage.content[lastMessage.content.length - 1].time;
+  const isOrWasPoll = currentMessage.message.includes(POLL_MESSAGE_PREFIX)
+    || lastMessage.message.includes(POLL_MESSAGE_PREFIX);
+  const groupingWindow = isOrWasPoll ? 0 : GROUPING_MESSAGES_WINDOW;
+
   if (lastMessage.sender === currentMessage.sender
-    && (currentMessage.timestamp - timeOfLastMessage) <= GROUPING_MESSAGES_WINDOW) {
+    && (currentMessage.timestamp - timeOfLastMessage) <= groupingWindow) {
     lastMessage.content.push(currentMessage.content.pop());
     return previous;
   }
diff --git a/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx b/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx
index 1ee8c4f7bcaf85badc1742bdd80c08ed8bd9dd05..df1383d4d005d7b194f8c5f9079d34b868bcad28 100644
--- a/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/error-screen/component.jsx
@@ -4,6 +4,7 @@ import { defineMessages, injectIntl } from 'react-intl';
 import { Meteor } from 'meteor/meteor';
 import { Session } from 'meteor/session';
 import AudioManager from '/imports/ui/services/audio-manager';
+import logger from '/imports/startup/client/logger';
 import { styles } from './styles';
 
 const intlMessages = defineMessages({
@@ -42,8 +43,10 @@ const defaultProps = {
 
 class ErrorScreen extends PureComponent {
   componentDidMount() {
+    const { code } = this.props;
     AudioManager.exitAudio();
     Meteor.disconnect();
+    logger.error({ logCode: 'startup_client_usercouldnotlogin_error' }, `User could not log in HTML5, hit ${code}`);
   }
 
   render() {
diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/custom-players/arc-player.jsx b/bigbluebutton-html5/imports/ui/components/external-video-player/custom-players/arc-player.jsx
index 0b9e14ef53c9dc69ee344414979be39fa6f5083e..fa1a1920214a8fc004985596ebf74a958a71c6fd 100644
--- a/bigbluebutton-html5/imports/ui/components/external-video-player/custom-players/arc-player.jsx
+++ b/bigbluebutton-html5/imports/ui/components/external-video-player/custom-players/arc-player.jsx
@@ -1,7 +1,7 @@
 import loadScript from 'load-script';
 import React, { Component } from 'react'
 
-const MATCH_URL = new RegExp("https?:\/\/(\\w+)\.(instructuremedia.com)(\/embed)?\/([-abcdef0-9]+)");
+const MATCH_URL = new RegExp("https?:\/\/(\\w+)[.](instructuremedia.com)(\/embed)?\/([-abcdef0-9]+)");
 
 const SDK_URL = 'https://files.instructuremedia.com/instructure-media-script/instructure-media-1.1.0.js';
 
diff --git a/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx b/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx
index 73839c248b65d28273a8ea1f7ea0ddcc3c9be7bd..4f31a9557e9e891520eac50bca28621cc86c721a 100755
--- a/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx
@@ -88,6 +88,7 @@ const propTypes = {
     formatMessage: PropTypes.func.isRequired,
   }).isRequired,
   code: PropTypes.string.isRequired,
+  reason: PropTypes.string.isRequired,
 };
 
 class MeetingEnded extends PureComponent {
@@ -168,14 +169,12 @@ class MeetingEnded extends PureComponent {
   }
 
   render() {
-    const { intl, code } = this.props;
-    const {
-      selected,
-    } = this.state;
+    const { code, intl, reason } = this.props;
+    const { selected } = this.state;
 
     const noRating = selected <= 0;
 
-    logger.info({ logCode: 'meeting_ended_code', extraInfo: { endedCode: code } }, 'Meeting ended component');
+    logger.info({ logCode: 'meeting_ended_code', extraInfo: { endedCode: code, reason } }, 'Meeting ended component');
 
     return (
       <div className={styles.parent}>
@@ -183,7 +182,7 @@ class MeetingEnded extends PureComponent {
           <div className={styles.content}>
             <h1 className={styles.title}>
               {
-                intl.formatMessage(intlMessage[code] || intlMessage[430])
+                intl.formatMessage(intlMessage[reason] || intlMessage[430])
               }
             </h1>
             <div className={styles.text}>
diff --git a/bigbluebutton-html5/imports/ui/components/poll/live-result/component.jsx b/bigbluebutton-html5/imports/ui/components/poll/live-result/component.jsx
index 1070f028709b916140584a8b3e360e9652c4aaf2..0aa6f2ace9fb8bc84bb35c61ec1e8962807ad0b4 100644
--- a/bigbluebutton-html5/imports/ui/components/poll/live-result/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/poll/live-result/component.jsx
@@ -194,12 +194,15 @@ class LiveResult extends PureComponent {
                 Session.set('pollInitiated', false);
                 Service.publishPoll();
                 const { answers, numRespondents } = currentPoll;
-
+                let responded = 0;
                 let resultString = 'bbb-published-poll-\n';
-                answers.forEach((item) => {
-                  const pct = Math.round(item.numVotes / numRespondents * 100);
+                answers.map((item) => {
+                  responded += item.numVotes;
+                  return item;
+                }).map((item) => {
+                  const numResponded = responded === numRespondents ? numRespondents : responded;
+                  const pct = Math.round(item.numVotes / numResponded * 100);
                   const pctFotmatted = `${Number.isNaN(pct) ? 0 : pct}%`;
-
                   resultString += `${item.key}: ${item.numVotes || 0} | ${pctFotmatted}\n`;
                 });
 
diff --git a/bigbluebutton-html5/imports/ui/components/poll/service.js b/bigbluebutton-html5/imports/ui/components/poll/service.js
index b08eb3b2b30f2a96b6440165364759068c8c0861..9f3fca29061212137c28e0fd62d850dab5e8eb4f 100644
--- a/bigbluebutton-html5/imports/ui/components/poll/service.js
+++ b/bigbluebutton-html5/imports/ui/components/poll/service.js
@@ -59,7 +59,7 @@ const sendGroupMessage = (message) => {
     color: '0',
     correlationId: `${PUBLIC_CHAT_SYSTEM_ID}-${Date.now()}`,
     sender: {
-      id: PUBLIC_CHAT_SYSTEM_ID,
+      id: Auth.userID,
       name: '',
     },
     message,
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx
index cda84f25cc05e77fb8b29b9b750c1b48fbfab465..e084ee858fbbf050cc99216f2fc06de0245cdd99 100755
--- a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx
@@ -11,6 +11,7 @@ import AnnotationGroupContainer from '../whiteboard/annotation-group/container';
 import PresentationOverlayContainer from './presentation-overlay/container';
 import Slide from './slide/component';
 import { styles } from './styles.scss';
+import toastStyles from '/imports/ui/components/toast/styles';
 import MediaService, { shouldEnableSwapLayout } from '../media/service';
 import PresentationCloseButton from './presentation-close-button/component';
 import DownloadPresentationButton from './download-presentation-button/component';
@@ -29,6 +30,10 @@ const intlMessages = defineMessages({
     id: 'app.presentation.notificationLabel',
     description: 'label displayed in toast when presentation switches',
   },
+  downloadLabel: {
+    id: 'app.presentation.downloadLabel',
+    description: 'label for downloadable presentations',
+  },
 });
 
 const ALLOW_FULLSCREEN = Meteor.settings.public.app.allowFullscreen;
@@ -131,6 +136,7 @@ class PresentationArea extends PureComponent {
       restoreOnUpdate,
       layoutContextDispatch,
       layoutContextState,
+      userIsPresenter,
     } = this.props;
 
     const { numUsersVideo } = layoutContextState;
@@ -172,16 +178,36 @@ class PresentationArea extends PureComponent {
       }
     }
 
-    if (prevProps.currentPresentation.name !== currentPresentation.name) {
+    const downloadableOn = !prevProps.currentPresentation.downloadable
+      && currentPresentation.downloadable;
+
+    const shouldCloseToast = !(currentPresentation.downloadable && !userIsPresenter);
+
+    if (
+      prevProps.currentPresentation.name !== currentPresentation.name
+      || (downloadableOn && !userIsPresenter)
+    ) {
       if (this.currentPresentationToastId) {
-        return toast.update(this.currentPresentationToastId, {
+        toast.update(this.currentPresentationToastId, {
+          autoClose: shouldCloseToast,
           render: this.renderCurrentPresentationToast(),
         });
+      } else {
+        this.currentPresentationToastId = toast(this.renderCurrentPresentationToast(), {
+          onClose: () => { this.currentPresentationToastId = null; },
+          autoClose: shouldCloseToast,
+          className: toastStyles.actionToast,
+        });
       }
+    }
+
+    const downloadableOff = prevProps.currentPresentation.downloadable
+      && !currentPresentation.downloadable;
 
-      this.currentPresentationToastId = toast(this.renderCurrentPresentationToast(), {
-        onClose: () => { this.currentPresentationToastId = null; },
+    if (this.currentPresentationToastId && downloadableOff) {
+      toast.update(this.currentPresentationToastId, {
         autoClose: true,
+        render: this.renderCurrentPresentationToast(),
       });
     }
 
@@ -670,7 +696,10 @@ class PresentationArea extends PureComponent {
   }
 
   renderCurrentPresentationToast() {
-    const { intl, currentPresentation } = this.props;
+    const {
+      intl, currentPresentation, userIsPresenter, downloadPresentationUri,
+    } = this.props;
+    const { downloadable } = currentPresentation;
 
     return (
       <div className={styles.innerToastWrapper}>
@@ -679,10 +708,28 @@ class PresentationArea extends PureComponent {
             <Icon iconName="presentation" />
           </div>
         </div>
+
         <div className={styles.toastTextContent} data-test="toastSmallMsg">
           <div>{`${intl.formatMessage(intlMessages.changeNotification)}`}</div>
           <div className={styles.presentationName}>{`${currentPresentation.name}`}</div>
         </div>
+
+        {downloadable && !userIsPresenter
+          ? (
+            <span className={styles.toastDownload}>
+              <div className={toastStyles.separator} />
+              <a
+                className={styles.downloadBtn}
+                aria-label={`${intl.formatMessage(intlMessages.downloadLabel)} ${currentPresentation.name}`}
+                href={downloadPresentationUri}
+                target="_blank"
+                rel="noopener noreferrer"
+              >
+                {intl.formatMessage(intlMessages.downloadLabel)}
+              </a>
+            </span>
+          ) : null
+        }
       </div>
     );
   }
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/styles.scss b/bigbluebutton-html5/imports/ui/components/presentation/styles.scss
index d7efec3bc5d2f1fe856e3ca6c5ddc5eb6f650db7..fdf85c3c894c8b633929d5d47f64bd5c9887e988 100644
--- a/bigbluebutton-html5/imports/ui/components/presentation/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/presentation/styles.scss
@@ -4,6 +4,7 @@
 :root {
   --innerToastWidth: 17rem;
   --iconWrapperSize: 2rem;
+  --toast-icon-side: 40px;
 }
 
 .enter {
@@ -93,14 +94,17 @@
 }
 
 .innerToastWrapper {
-  display: flex;
-  flex-direction: row;
   width: var(--innerToastWidth);
 }
 
 .toastTextContent {
   position: relative;
   overflow: hidden;
+  margin-top: var(--sm-padding-y);
+
+  > div:first-of-type {
+    font-weight: bold;
+  }
 }
 
 .presentationName {
@@ -108,6 +112,13 @@
   overflow: hidden;
 }
 
+.toastMessage {
+  font-size: var(--font-size-small);
+  margin-top: var(--toast-margin);
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
 .toastIcon {
   margin-right: var(--sm-padding-x);
   [dir="rtl"] & {
@@ -118,21 +129,37 @@
 
 .iconWrapper {
   background-color: var(--color-primary);
-  width: var(--iconWrapperSize);
-  height: var(--iconWrapperSize);
+  width: var(--toast-icon-side);
+  height: var(--toast-icon-side);
   border-radius: 50%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
 
   > i {
     position: relative;
-    left: var(--md-padding-y);
-    top: var(--sm-padding-y);
-    font-size: var(--font-size-base);
     color: var(--color-white);
+    font-size: var(--font-size-larger);
+  }
+}
+
+.toastDownload {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
 
-    [dir="rtl"] & {
-      left: 0;
-      right: var(--md-padding-y);
-   }
+  a {
+    color: var(--color-primary);
+    cursor: pointer;
+    text-decoration: none;
+
+    &:focus,
+    &:hover,
+    &:active {
+      color: var(--color-primary);
+      box-shadow: 0;
+    }
   }
 }
 
diff --git a/bigbluebutton-html5/imports/ui/components/status-notifier/component.jsx b/bigbluebutton-html5/imports/ui/components/status-notifier/component.jsx
index a337150da8595e686bd32bcf693680b0877ba430..78f822c1fbe5b18ebf25412fd42eed9c66ca32fa 100644
--- a/bigbluebutton-html5/imports/ui/components/status-notifier/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/status-notifier/component.jsx
@@ -5,6 +5,7 @@ import { toast } from 'react-toastify';
 import Icon from '/imports/ui/components/icon/component';
 import Button from '/imports/ui/components/button/component';
 import { ENTER } from '/imports/utils/keyCodes';
+import toastStyles from '/imports/ui/components/toast/styles';
 import { styles } from './styles';
 
 const messages = defineMessages({
@@ -71,7 +72,7 @@ class StatusNotifier extends Component {
             autoClose: false,
             closeOnClick: false,
             closeButton: false,
-            className: styles.raisedHandsToast,
+            className: toastStyles.actionToast,
           });
         }
         break;
@@ -161,7 +162,7 @@ class StatusNotifier extends Component {
           <div>{intl.formatMessage(messages.raisedHandsTitle)}</div>
           {formattedRaisedHands}
         </div>
-        <div className={styles.separator} />
+        <div className={toastStyles.separator} />
         <Button
           className={styles.clearBtn}
           label={intl.formatMessage(messages.lowerHandsLabel)}
diff --git a/bigbluebutton-html5/imports/ui/components/status-notifier/styles.scss b/bigbluebutton-html5/imports/ui/components/status-notifier/styles.scss
index 5f396dfcc917f95d91cba12c069a8ea94a8c8887..d5f4978fdcc6aa48bc4f341695ddb773d6d2b84c 100644
--- a/bigbluebutton-html5/imports/ui/components/status-notifier/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/status-notifier/styles.scss
@@ -3,7 +3,6 @@
 
 :root {
   --iconWrapperSize: 40px;
-  --icon-offset: .6rem;
   --innerToastWidth: 17rem;
   --toast-margin: .5rem;
   --toast-icon-side: 40px;
@@ -87,23 +86,6 @@
   padding: 5px 0;
 }
 
-.raisedHandsToast {
-  display: flex;
-  position: relative;
-  margin-bottom: var(--sm-padding-x);
-  padding: var(--md-padding-x);
-  border-radius: var(--border-radius);
-  box-shadow: 0 var(--border-size-small) 10px 0 rgba(0, 0, 0, 0.1), 0 var(--border-size) 15px 0 rgba(0, 0, 0, 0.05);
-  justify-content: space-between;
-  color: var(--color-text);
-  background-color: var(--color-white);
-  -webkit-animation-duration: 0.75s;
-  animation-duration: 0.75s;
-  -webkit-animation-fill-mode: both;
-  animation-fill-mode: both;
-  max-width: var(--toast-max-width);
-}
-
 .avatar:hover,
 .avatar:focus {
     border: solid var(--border-size) var(--color-gray-lighter);
@@ -118,8 +100,8 @@
   > i {
     position: relative;
     color: var(--color-white);
-    top: var(--icon-offset);
-    left: var(--icon-offset);
+    top: var(--toast-margin);
+    left: var(--toast-margin);
     font-size: var(--font-size-xl);
   
     [dir="rtl"] & {
diff --git a/bigbluebutton-html5/imports/ui/components/toast/styles.scss b/bigbluebutton-html5/imports/ui/components/toast/styles.scss
index a5ff87922320c9a71a9da1632519d2580ad3ef08..5749ed0116b3152eb3b060cb91d6f1155aeec3f3 100755
--- a/bigbluebutton-html5/imports/ui/components/toast/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/toast/styles.scss
@@ -144,21 +144,28 @@
   }
 }
 
-.toast {
+.toast ,
+.actionToast {
   position: relative;
   margin-bottom: var(--sm-padding-x);
-  padding: var(--md-padding-x);
+  padding: var(--sm-padding-x);
   border-radius: var(--border-radius);
   box-shadow: 0 var(--border-size-small) 10px 0 rgba(0, 0, 0, 0.1), 0 var(--border-size) 15px 0 rgba(0, 0, 0, 0.05);
   display: flex;
   justify-content: space-between;
-  cursor: pointer;
   color: var(--color-text);
-  background-color: var(--background);
+  -webkit-animation-duration: 0.75s;
   animation-duration: 0.75s;
+  -webkit-animation-fill-mode: both;
   animation-fill-mode: both;
   max-width: var(--toast-max-width);
+  min-width: var(--toast-max-width);
   width: var(--toast-max-width);
+}
+
+.toast {
+  cursor: pointer;
+  background-color: var(--background);
 
   &:hover,
   &:focus {
@@ -166,6 +173,14 @@
   }
 }
 
+.actionToast {
+  background-color: var(--color-white);
+
+  i.close {
+    left: none !important;
+  }
+}
+
 .body {
   margin: auto auto;
   flex: 1;
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js
index 178deff67ddf2c3db946ac91250d9b223b6a38e3..1cc441d9a0257654f8f672a5ee7d1a66148ca3ff 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/service.js
+++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js
@@ -510,7 +510,12 @@ export const getUserNamesLink = () => {
     .map(u => u.name)
     .join('\r\n');
   const link = document.createElement('a');
-  link.setAttribute('download', `save-users-list-${Date.now()}.txt`);
+  const meeting = Meetings.findOne({ meetingId: Auth.meetingID },
+    { fields: { 'meetingProp.name': 1 } });
+  const date = new Date();
+  const time = `${date.getHours()}-${date.getMinutes()}`;
+  const dateString = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}_${time}`;
+  link.setAttribute('download', `bbb-${meeting.meetingProp.name}[users-list]_${dateString}.txt`);
   link.setAttribute(
     'href',
     `data: ${mimeType} ;charset=utf-16,${encodeURIComponent(userNameListString)}`,
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/service.js b/bigbluebutton-html5/imports/ui/components/video-provider/service.js
index c25a1cb3ddccb4b02b0c1e724cac57f1ba68f3fe..d251126a591cd80603b66f0e372e5bb5cdc8bfef 100755
--- a/bigbluebutton-html5/imports/ui/components/video-provider/service.js
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/service.js
@@ -229,7 +229,7 @@ class VideoService {
 
   filterModeratorOnly(streams) {
     const me = Users.findOne({ userId: Auth.userID });
-    const amIViewer = me.role === ROLE_VIEWER;
+    const amIViewer = me?.role === ROLE_VIEWER;
 
     if (amIViewer) {
       const moderators = Users.find(
@@ -245,7 +245,7 @@ class VideoService {
         const { userId } = stream;
 
         const isModerator = moderators.includes(userId);
-        const isMe = me.userId === userId;
+        const isMe = me?.userId === userId;
 
         if (isModerator || isMe) result.push(stream);
 
@@ -264,13 +264,13 @@ class VideoService {
   webcamsOnlyForModerator() {
     const m = Meetings.findOne({ meetingId: Auth.meetingID },
       { fields: { 'usersProp.webcamsOnlyForModerator': 1 } });
-    return m.usersProp ? m.usersProp.webcamsOnlyForModerator : false;
+    return m?.usersProp ? m.usersProp.webcamsOnlyForModerator : false;
   }
 
   hideUserList() {
     const m = Meetings.findOne({ meetingId: Auth.meetingID },
       { fields: { 'lockSettingsProps.hideUserList': 1 } });
-    return m.lockSettingsProps ? m.lockSettingsProps.hideUserList : false;
+    return m?.lockSettingsProps ? m.lockSettingsProps.hideUserList : false;
   }
 
   getInfo() {
diff --git a/bigbluebutton-html5/imports/ui/stylesheets/variables/typography.scss b/bigbluebutton-html5/imports/ui/stylesheets/variables/typography.scss
index bf83f8da395fa33535242c4244189bc263016e41..492a95241e8efe68fb598d2e72ba61e68c458f96 100644
--- a/bigbluebutton-html5/imports/ui/stylesheets/variables/typography.scss
+++ b/bigbluebutton-html5/imports/ui/stylesheets/variables/typography.scss
@@ -7,6 +7,7 @@
 
   --font-size-base: 1rem;
   --font-size-xl: 1.75rem;
+  --font-size-larger: 1.5rem;
   --font-size-large: 1.25rem;
   --font-size-md: 0.95rem; 
   --font-size-small: 0.875rem;
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json
index cc6ff15f39ec68c0c3ddd42ca91f5c63d6e96889..6457c594cfa6569e39707a0050bfc0056961db27 100755
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/private/locales/en.json
@@ -135,6 +135,7 @@
     "app.meeting.alertBreakoutEndsUnderMinutesSingular": "Breakout is closing in one minute.",
     "app.presentation.hide": "Hide presentation",
     "app.presentation.notificationLabel": "Current presentation",
+    "app.presentation.downloadLabel": "Download",
     "app.presentation.slideContent": "Slide Content",
     "app.presentation.startSlideContent": "Slide content start",
     "app.presentation.endSlideContent": "Slide content end",