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",