diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/CreateGroupChatReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/CreateGroupChatReqMsgHdlr.scala index 368383fb5c121b91db668cbb91b271b801b70337..fddebc0b0be64a68586d76bf1095cac897ce5f7d 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/CreateGroupChatReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/CreateGroupChatReqMsgHdlr.scala @@ -26,7 +26,14 @@ trait CreateGroupChatReqMsgHdlr extends SystemConfiguration { if (user.role != Roles.MODERATOR_ROLE) { if (msg.body.access == GroupChatAccess.PRIVATE) { val permissions = MeetingStatus2x.getPermissions(liveMeeting.status) - chatLocked = user.locked && permissions.disablePrivChat + val modMembers = msg.body.users.filter(userId => Users2x.findWithIntId(liveMeeting.users2x, userId) match { + case Some(user) => user.role == Roles.MODERATOR_ROLE + case None => false + }) + // don't lock creation of private chats that involve a moderator + if (modMembers.length == 0) { + chatLocked = user.locked && permissions.disablePrivChat + } } else { chatLocked = true } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/SendGroupChatMessageMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/SendGroupChatMessageMsgHdlr.scala index 09221ea54a3e82bab496dc030d24371a982258d2..b2b96a2c150f71e9a72dce42da90e717722bb791 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/SendGroupChatMessageMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/groupchats/SendGroupChatMessageMsgHdlr.scala @@ -28,7 +28,14 @@ trait SendGroupChatMessageMsgHdlr { if (user.role != Roles.MODERATOR_ROLE && user.locked) { val permissions = MeetingStatus2x.getPermissions(liveMeeting.status) if (groupChat.access == GroupChatAccess.PRIVATE) { - chatLocked = permissions.disablePrivChat + val modMembers = groupChat.users.filter(cu => Users2x.findWithIntId(liveMeeting.users2x, cu.id) match { + case Some(user) => user.role == Roles.MODERATOR_ROLE + case None => false + }) + // don't lock private chats that involve a moderator + if (modMembers.length == 0) { + chatLocked = permissions.disablePrivChat + } } else { chatLocked = permissions.disablePubChat } diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatOptionsTab.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatOptionsTab.mxml old mode 100644 new mode 100755 index bc78dfbaa514dd08ae6dbb3eabd871a8642df5d9..3e6e171838e66205cd595e68833490ddea8d54d2 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatOptionsTab.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatOptionsTab.mxml @@ -217,10 +217,12 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } private function refreshListStatus():void { - - if (UsersUtil.amIModerator() || UsersUtil.amIPresenter()) return; // Settings only affect viewers. - - usersList.enabled = ! LiveMeeting.inst().me.disableMyPrivateChat; + if (LiveMeeting.inst().me.disableMyPrivateChat) { + handler.enableModeratorsFilter(); + } else { + handler.disableModeratorsFilter(); + } + users = removeMe(handler.users); } public function sendSaveEvent():void{ diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatTab.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatTab.mxml index 5c69d8d21684a287769ce8b8c68ddef0496ab838..60aeefe5de2dfe7c6d2a56ade5521c77e4390d09 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatTab.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatTab.mxml @@ -69,6 +69,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.core.events.LockControlEvent; import org.bigbluebutton.core.events.UserStatusChangedEvent; import org.bigbluebutton.core.model.LiveMeeting; + import org.bigbluebutton.core.model.users.User2x; import org.bigbluebutton.main.events.BBBEvent; import org.bigbluebutton.main.events.ShortcutEvent; import org.bigbluebutton.main.events.UserJoinedEvent; @@ -87,13 +88,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.modules.chat.model.ChatOptions; import org.bigbluebutton.modules.chat.model.GroupChat; import org.bigbluebutton.modules.chat.vo.ChatMessageVO; + import org.bigbluebutton.modules.chat.vo.GroupChatUser; import org.bigbluebutton.modules.polling.events.StartCustomPollEvent; import org.bigbluebutton.util.i18n.ResourceUtil; private static const LOGGER:ILogger = getClassLogger(ChatTab); public var publicChat:Boolean = false; - public var chatWithUserID:String; public var chatWithUsername:String public var chatId: String = null; public var parentWindowId:String = null; @@ -609,7 +610,18 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. if (publicChat) { txtMsgArea.enabled = sendBtn.enabled = !LiveMeeting.inst().me.disableMyPublicChat; } else { - txtMsgArea.enabled = sendBtn.enabled = !LiveMeeting.inst().me.disableMyPrivateChat || LiveMeeting.inst().users.getUser(chatWithUserID).role == Role.MODERATOR; + var chattingWithMod:Boolean = false; + var gc:GroupChat = LiveMeeting.inst().chats.getGroupChat(chatId); + for (var i:int = 0; i < gc.users.length; i++) { + var chatUser:GroupChatUser = gc.users[i] as GroupChatUser; + if (chatUser.id != UsersUtil.getMyUserID()) { + var user:User2x = UsersUtil.getUser(chatUser.id); + if (user && user.role == Role.MODERATOR) { + chattingWithMod = true; + } + } + } + txtMsgArea.enabled = sendBtn.enabled = !LiveMeeting.inst().me.disableMyPrivateChat || chattingWithMod; } } diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatView.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatView.mxml index 0564867706b1b34a91bbc87cb1393369c8ca4b07..9febf4c8a4d24abcd3eda8cf53392f096521ae5a 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatView.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatView.mxml @@ -340,7 +340,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. var chatBox:ChatTab = new ChatTab(); chatBox.name = groupChatId; - chatBox.chatWithUserID = groupChatId; chatBox.parentWindowId = windowId; chatBox.tabIndexer.startIndex = tabIndexer.startIndex + 10; chatBox.chatMessages = new ChatConversation(groupChatId); diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/views/QuickPollButton.as b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/views/QuickPollButton.as index a608823bd216cc2e1a6c3d527f0eb4b4d3ec2208..34f0d3611fad299d10442ce56fb5a6ae372d852c 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/polling/views/QuickPollButton.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/polling/views/QuickPollButton.as @@ -9,19 +9,27 @@ package org.bigbluebutton.modules.polling.views { import org.bigbluebutton.modules.present.events.PageLoadedEvent; import org.bigbluebutton.modules.present.model.Page; import org.bigbluebutton.modules.present.model.PresentationModel; + import org.bigbluebutton.modules.present.model.PresentationPodManager; import org.bigbluebutton.util.i18n.ResourceUtil; public class QuickPollButton extends Button { - private static const LOGGER:ILogger = getClassLogger(QuickPollButton); + private static const LOGGER:ILogger = getClassLogger(QuickPollButton); + + private var currentPageId:String; + private var currentPodId:String; override public function set visible(vsb:Boolean):void { if (vsb) { // This button should only be visible when there is a polling at the current slide's text -// var page:Page = PresentationModel.getInstance().getCurrentPage(); -// super.visible = page != null ? parseSlideText(page.txtData) : false; - } else { - super.visible = false; + var presentationModel:PresentationModel = PresentationPodManager.getInstance().getPod(PresentationPodManager.DEFAULT_POD_ID); + if (presentationModel != null) { + var page:Page = presentationModel.getCurrentPage(); + super.visible = page != null ? parseSlideText(page.txtData) : false; + return; + } } + + super.visible = false; } public function QuickPollButton() { @@ -34,7 +42,10 @@ package org.bigbluebutton.modules.polling.views { } private function handlePageLoadedEvent(e:PageLoadedEvent):void { - visible = UsersUtil.amIPresenter(); + // Only revalidate when it's the default pod that loaded a page + if (e.podId == PresentationPodManager.DEFAULT_POD_ID) { + visible = UsersUtil.amIPresenter(); + } } private function parseSlideText(text:String):Boolean { diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/PresentationWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/PresentationWindow.mxml index c7bae41b02eb024b00da1e1517421883add3f5d0..e39d45ca49c74be2eb16e205ccbe9a529fdddd2e 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/PresentationWindow.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/PresentationWindow.mxml @@ -609,9 +609,12 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. if (this.podId == PresentationPodManager.DEFAULT_POD_ID) { // only allow polling from the default pod pollStartBtn.visible = showButtons; + quickPollBtn.visible = showButtons; + } else { + pollStartBtn.visible = false; + quickPollBtn.visible = false; } - quickPollBtn.visible = showButtons; navigationControls.visible = showButtons; zoomControls.visible = showButtons; diff --git a/bigbluebutton-config/bin/bbb-conf b/bigbluebutton-config/bin/bbb-conf index b9ec66e231843ebd291fe19ba79b3fe047a9e630..b77db5a6423fa528586be00e0f8e677cfd33ae81 100755 --- a/bigbluebutton-config/bin/bbb-conf +++ b/bigbluebutton-config/bin/bbb-conf @@ -1315,11 +1315,11 @@ check_state() { check_no_value server_name /etc/nginx/sites-available/bigbluebutton $BBB_WEB COUNT=0 - while [ $COUNT -lt 30 ]; do + while [ $COUNT -lt 45 ]; do let COUNT=COUNT+1 timeout 1s wget $PROTOCOL://$BBB_WEB/bigbluebutton/api -O - --quiet | grep -q SUCCESS if [ $? -eq 0 ]; then - let COUNT=40 + let COUNT=45 else echo -n "." sleep 1 diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js index b9b1c6cc9b388177164d15de3b15f21619d7a084..75832fc0e6f03abb79667748069ef3aae56c1163 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js @@ -46,7 +46,7 @@ export default function addMeeting(meeting) { recordProp: Match.ObjectIncluding({ allowStartStopRecording: Boolean, autoStartRecording: Boolean, - record: Boolean + record: Boolean, }), password: { viewerPass: String, @@ -66,6 +66,8 @@ export default function addMeeting(meeting) { metadataProp: Object, }); + const newMeeting = meeting; + const selector = { meetingId, }; @@ -81,10 +83,26 @@ export default function addMeeting(meeting) { setBy: 'temp', }; + newMeeting.welcomeProp.welcomeMsg = newMeeting.welcomeProp.welcomeMsg.replace( + 'href="event:', + 'href="', + ); + + const insertBlankTarget = (s, i) => `${s.substr(0, i)} target="_blank"${s.substr(i)}`; + const linkWithoutTarget = new RegExp('<a href="(.*?)">', 'g'); + linkWithoutTarget.test(newMeeting.welcomeProp.welcomeMsg); + + if (linkWithoutTarget.lastIndex > 0) { + newMeeting.welcomeProp.welcomeMsg = insertBlankTarget( + newMeeting.welcomeProp.welcomeMsg, + linkWithoutTarget.lastIndex - 1, + ); + } + const modifier = { $set: Object.assign( { meetingId }, - flat(meeting, { safe: true }), + flat(newMeeting, { safe: true }), { lockSettingsProp }, ), }; diff --git a/bigbluebutton-html5/imports/api/polls/server/modifiers/addPoll.js b/bigbluebutton-html5/imports/api/polls/server/modifiers/addPoll.js index 6ecd2253a0f30293a3aa9d83c16d376ae471d3b5..5b3e0d6150759abe827e0beac22a90c606a9f7bb 100644 --- a/bigbluebutton-html5/imports/api/polls/server/modifiers/addPoll.js +++ b/bigbluebutton-html5/imports/api/polls/server/modifiers/addPoll.js @@ -20,6 +20,7 @@ export default function addPoll(meetingId, requesterId, poll) { const userSelector = { meetingId, userId: { $ne: requesterId }, + clientType: { $ne: 'dial-in-user' }, }; const userIds = Users.find(userSelector) diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx index 21f57eb0eecee9a313aaee7dbb7f198d0c576082..1a5d64be2bb43a38f7e8260c2a9f4acf9073eeca 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx @@ -1,7 +1,7 @@ import _ from 'lodash'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, intlShape } from 'react-intl'; +import { defineMessages, intlShape } from 'react-intl'; import Button from '/imports/ui/components/button/component'; import Dropdown from '/imports/ui/components/dropdown/component'; import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component'; @@ -315,4 +315,4 @@ class ActionsDropdown extends Component { ActionsDropdown.propTypes = propTypes; -export default withShortcutHelper(withModalMounter(injectIntl(ActionsDropdown)), 'openActions'); +export default withShortcutHelper(withModalMounter(ActionsDropdown), 'openActions'); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index 600d9ff762bf860cf9647d0464200d21bb3b936c..56f8439d820fae6ba85ca64c7c8c7682e7d70d24 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -3,6 +3,7 @@ import cx from 'classnames'; import { styles } from './styles.scss'; import DesktopShare from './desktop-share/component'; import ActionsDropdown from './actions-dropdown/component'; +import QuickPollDropdown from './quick-poll-dropdown/component'; import AudioControlsContainer from '../audio/audio-controls/container'; import JoinVideoOptionsContainer from '../video-provider/video-button/container'; @@ -33,6 +34,9 @@ class ActionsBar extends React.PureComponent { sendInvitation, getBreakouts, handleTakePresenter, + intl, + currentSlidHasContent, + parseCurrentSlideContent, isSharingVideo, } = this.props; @@ -68,9 +72,18 @@ class ActionsBar extends React.PureComponent { sendInvitation, getBreakouts, handleTakePresenter, + intl, isSharingVideo, }} /> + <QuickPollDropdown + {...{ + currentSlidHasContent, + intl, + isUserPresenter, + parseCurrentSlideContent, + }} + /> </div> <div className={ @@ -96,7 +109,7 @@ class ActionsBar extends React.PureComponent { /> </div> <div className={styles.right}> - { isLayoutSwapped + {isLayoutSwapped ? ( <PresentationOptionsContainer toggleSwapLayout={toggleSwapLayout} diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx index 87751b6139a157b00e888041686ac55acd0a0d55..65a7a21512c899b4903f82c9e5e87f61a961bdcb 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx @@ -1,8 +1,10 @@ import React from 'react'; import { withTracker } from 'meteor/react-meteor-data'; +import { injectIntl } from 'react-intl'; import getFromUserSettings from '/imports/ui/services/users-settings'; import Meetings from '/imports/api/meetings'; import Auth from '/imports/ui/services/auth'; +import PresentationService from '/imports/ui/components/presentation/service'; import ActionsBar from './component'; import Service from './service'; import VideoService from '../video-provider/service'; @@ -49,6 +51,8 @@ export default withTracker(() => { getBreakouts: Service.getBreakouts, getUsersNotAssigned: Service.getUsersNotAssigned, handleTakePresenter: Service.takePresenterRole, + currentSlidHasContent: PresentationService.currentSlidHasContent(), + parseCurrentSlideContent: PresentationService.parseCurrentSlideContent, isSharingVideo: Service.isSharingVideo(), }; -})(ActionsBarContainer); +})(injectIntl(ActionsBarContainer)); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3ce81f32a1086c064b321f4d2c43335ccb0aaff9 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx @@ -0,0 +1,107 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, intlShape } from 'react-intl'; +import _ from 'lodash'; +import { makeCall } from '/imports/ui/services/api'; +import Button from '/imports/ui/components/button/component'; +import Dropdown from '/imports/ui/components/dropdown/component'; +import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component'; +import DropdownContent from '/imports/ui/components/dropdown/content/component'; +import DropdownList from '/imports/ui/components/dropdown/list/component'; +import DropdownListItem from '/imports/ui/components/dropdown/list/item/component'; +import { styles } from '../styles'; + +const intlMessages = defineMessages({ + quickPollLabel: { + id: 'app.poll.quickPollTitle', + description: 'Quick poll button title', + }, + trueOptionLabel: { + id: 'app.poll.t', + description: 'Poll true option value', + }, + falseOptionLabel: { + id: 'app.poll.f', + description: 'Poll false option value', + }, + yesOptionLabel: { + id: 'app.poll.y', + description: 'Poll yes option value', + }, + noOptionLabel: { + id: 'app.poll.n', + description: 'Poll no option value', + }, +}); + +const propTypes = { + intl: intlShape.isRequired, + parseCurrentSlideContent: PropTypes.func.isRequired, + isUserPresenter: PropTypes.bool.isRequired, + +}; + +const handleClickQuickPoll = (slideId, poll) => { + const { type } = poll; + Session.set('openPanel', 'poll'); + Session.set('forcePollOpen', true); + + makeCall('startPoll', type, slideId); +}; + + +const getAvailableQuickPolls = (slideId, parsedSlides) => parsedSlides.map((poll) => { + const { poll: label, type } = poll; + let itemLabel = label; + + if (type !== 'YN' && type !== 'TF') { + const { options } = itemLabel; + itemLabel = options.join('/').replace(/[\n.)]/g, ''); + } + + return ( + <DropdownListItem + label={itemLabel} + key={_.uniqueId('quick-poll-item')} + onClick={() => handleClickQuickPoll(slideId, poll)} + />); +}); + +const QuickPollDropdown = (props) => { + const { isUserPresenter, intl, parseCurrentSlideContent } = props; + const parsedSlide = parseCurrentSlideContent( + intl.formatMessage(intlMessages.yesOptionLabel), + intl.formatMessage(intlMessages.noOptionLabel), + intl.formatMessage(intlMessages.trueOptionLabel), + intl.formatMessage(intlMessages.falseOptionLabel), + ); + + const { slideId, quickPollOptions } = parsedSlide; + + return isUserPresenter && quickPollOptions && quickPollOptions.length ? ( + <Dropdown> + <DropdownTrigger tabIndex={0}> + <Button + aria-label={intl.formatMessage(intlMessages.quickPollLabel)} + circle + className={styles.button} + color="primary" + hideLabel + icon="polling" + label={intl.formatMessage(intlMessages.quickPollLabel)} + onClick={() => null} + size="lg" + /> + </DropdownTrigger> + <DropdownContent placement="top left"> + <DropdownList> + {getAvailableQuickPolls(slideId, quickPollOptions)} + </DropdownList> + </DropdownContent> + </Dropdown> + ) : null; +}; + +QuickPollDropdown.propTypes = propTypes; + +export default QuickPollDropdown; diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js index 4ce6bae649357e37a72c7b72ae3ab8acc0537d96..ddb02f7550b9e91734b2ac77a7d25ceeceaba5ac 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js @@ -26,7 +26,7 @@ const filterUsersNotAssigned = filterBreakoutUsers(currentBreakoutUsers); const mapUsersToNotAssined = mapFunction => users => users.map(mapFunction); -const flatUsersArray = usersArray => usersArray.reduce((acc, users) => [...acc, users], []); +const flatUsersArray = usersArray => usersArray.reduce((acc, users) => [...acc, ...users], []); /* The concept of pipe is simple diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss index b7d8f42715468ff98d10381edc2919b3896c8ea6..3d84e405f2d47efc1ad00357fbe6ce9e9a085826 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss @@ -33,21 +33,19 @@ &.glow { border-radius: 50%; - + [style~="--enableAnimation:1;"] & { animation: pulse 1s infinite ease-in; } + [style~="--enableAnimation:0;"] & span { + content: ''; + outline: none !important; + background-clip: padding-box; + box-shadow: 0 0 0 4px rgba(255,255,255,.5); + } } } -[style~="--enableAnimation:0;"] .button.glow span { - content: ''; - outline: none !important; - background-clip: padding-box; - border: var(--border-size-large) solid transparent; - box-shadow: 0 0 0 var(--border-size) rgba(255,255,255,.5); -} - @keyframes pulse { 0% { box-shadow: 0 0 0 0 white; diff --git a/bigbluebutton-html5/imports/ui/components/audio/echo-test/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/echo-test/component.jsx index ae9266b58cdbfca6367af415876b1dc9db167fc8..8c6aef33c212c29026f531e1c1c8b4bdc2ab85ef 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/echo-test/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/echo-test/component.jsx @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { Session } from 'meteor/session'; import Button from '/imports/ui/components/button/component'; import { defineMessages, intlShape, injectIntl } from 'react-intl'; import { styles } from './styles'; @@ -39,6 +40,14 @@ class EchoTest extends Component { this.handleNo = props.handleNo.bind(this); } + componentDidMount() { + Session.set('inEchoTest', true); + } + + componentWillUnmount() { + Session.set('inEchoTest', false); + } + render() { const { intl, diff --git a/bigbluebutton-html5/imports/ui/components/chat/service.js b/bigbluebutton-html5/imports/ui/components/chat/service.js index 8730e413a51178bb0bdcb24b912e8812cae2e17e..aa0bc5e47054aa8b4f81a92efd6ce45a0d445249 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/service.js +++ b/bigbluebutton-html5/imports/ui/components/chat/service.js @@ -20,6 +20,8 @@ const PRIVATE_CHAT_TYPE = CHAT_CONFIG.type_private; const PUBLIC_CHAT_USER_ID = CHAT_CONFIG.system_userid; const PUBLIC_CHAT_CLEAR = CHAT_CONFIG.system_messages_keys.chat_clear; +const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; + const ScrollCollection = new Mongo.Collection(null); const UnsentMessagesCollection = new Mongo.Collection(null); @@ -135,11 +137,14 @@ const isChatLocked = (receiverID) => { const user = Users.findOne({ userId: Auth.userID }); if (meeting.lockSettingsProp !== undefined) { - const isPubChatLocked = meeting.lockSettingsProp.disablePubChat; - const isPrivChatLocked = meeting.lockSettingsProp.disablePrivChat; - - return mapUser(user).isLocked - && ((isPublic && isPubChatLocked) || (!isPublic && isPrivChatLocked)); + if (mapUser(user).isLocked) { + if (isPublic) { + return meeting.lockSettingsProp.disablePubChat; + } + const receivingUser = Users.findOne({ userId: receiverID }); + const receiverIsMod = receivingUser && receivingUser.role === ROLE_MODERATOR; + return !receiverIsMod && meeting.lockSettingsProp.disablePrivChat; + } } return false; diff --git a/bigbluebutton-html5/imports/ui/components/error-screen/styles.scss b/bigbluebutton-html5/imports/ui/components/error-screen/styles.scss index a329f8a7c09212bb5ef3bd19ec31db041134ee21..0758bbff572084a18f26f15ae94da27546c107d9 100644 --- a/bigbluebutton-html5/imports/ui/components/error-screen/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/error-screen/styles.scss @@ -48,6 +48,6 @@ } .button { - width: 9rem; + min-width: 9rem; height: 2rem; } \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/component.jsx b/bigbluebutton-html5/imports/ui/components/external-video-player/component.jsx index 6089016c5da43995a1210c4a07bbdb7e7bcd102e..b6a0fb3f510eefa6e26c7d40b0ff82443c54427b 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/component.jsx @@ -15,6 +15,10 @@ class VideoPlayer extends Component { this.playerState = PlayerState.UNSTARTED; this.presenterCommand = false; this.preventStateChange = false; + this.state = { + mutedByEchoTest: false, + }; + this.opts = { playerVars: { width: '100%', @@ -30,6 +34,7 @@ class VideoPlayer extends Component { this.handleResize = this.handleResize.bind(this); this.handleOnReady = this.handleOnReady.bind(this); this.handleStateChange = this.handleStateChange.bind(this); + this.changeState = this.changeState.bind(this); this.resizeListener = () => { setTimeout(this.handleResize, 0); }; @@ -39,8 +44,23 @@ class VideoPlayer extends Component { window.addEventListener('resize', this.resizeListener); } - componentDidUpdate(nextProps) { - if (!nextProps.videoId) { + componentDidUpdate(prevProps) { + const { inEchoTest } = this.props; + const { + mutedByEchoTest, + } = this.state; + + if (inEchoTest && !this.player.isMuted() && !mutedByEchoTest) { + this.player.mute(); + this.changeState(true); + } + + if (!inEchoTest && prevProps.inEchoTest && mutedByEchoTest) { + this.player.unMute(); + this.changeState(false); + } + + if (!prevProps.videoId) { clearInterval(this.syncInterval); } } @@ -53,6 +73,10 @@ class VideoPlayer extends Component { this.refPlayer = null; } + changeState(booleanValue) { + this.setState({ mutedByEchoTest: booleanValue }); + } + handleResize() { if (!this.player || !this.refPlayer) { return; diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/container.jsx b/bigbluebutton-html5/imports/ui/components/external-video-player/container.jsx index 9a6445c72d46594251503d2f82ffa729c8346924..aca54887bf628b7b778478cb63199ebba75739c5 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/container.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { defineMessages, injectIntl } from 'react-intl'; import { withTracker } from 'meteor/react-meteor-data'; +import { Session } from 'meteor/session'; import ExternalVideo from './component'; const intlMessages = defineMessages({ @@ -18,8 +19,9 @@ const ExternalVideoContainer = props => ( export default injectIntl(withTracker(({ params, intl, isPresenter }) => { const title = intl.formatMessage(intlMessages.title); - + const inEchoTest = Session.get('inEchoTest'); return { + inEchoTest, title, isPresenter, }; diff --git a/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx b/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx index cc89e76ece87407a1d53998f5f43257683d44ec2..7a4fd90b3a3f709a80763b8d5598657c56f00ba1 100755 --- a/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx @@ -8,7 +8,7 @@ import getFromUserSettings from '/imports/ui/services/users-settings'; import logoutRouteHandler from '/imports/utils/logoutRouteHandler'; import Rating from './rating/component'; import { styles } from './styles'; - +import logger from '/imports/startup/client/logger'; const intlMessage = defineMessages({ 410: { @@ -121,6 +121,9 @@ class MeetingEnded extends React.PureComponent { }, }; + // client logger + logger.info({ feedback: message, logCode: 'feedback' }, 'Feedback'); + fetch(url, options) .then(() => { logoutRouteHandler(); diff --git a/bigbluebutton-html5/imports/ui/components/note/component.jsx b/bigbluebutton-html5/imports/ui/components/note/component.jsx index f9e52d4e6034b098665ba2771dd7820cea7f22e8..7233ebc490dcd4e13f6f6a98d069625b0c623dd6 100644 --- a/bigbluebutton-html5/imports/ui/components/note/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/note/component.jsx @@ -15,6 +15,10 @@ const intlMessages = defineMessages({ id: 'app.note.title', description: 'Title for the shared notes', }, + tipLabel: { + id: 'app.note.tipLabel', + description: 'Label for tip on how to escape iframe', + }, }); const propTypes = { @@ -57,7 +61,11 @@ const Note = (props) => { <iframe title="etherpad" src={url} + aria-describedby="sharedNotesEscapeHint" /> + <span id="sharedNotesEscapeHint" className={styles.hint} aria-hidden> + {intl.formatMessage(intlMessages.tipLabel)} + </span> </div> ); }; diff --git a/bigbluebutton-html5/imports/ui/components/note/styles.scss b/bigbluebutton-html5/imports/ui/components/note/styles.scss index 8f9be377f1fc9c1bacc5135f43184b70feddd714..63f8f336873b438546134f040fb13fc19f74145a 100644 --- a/bigbluebutton-html5/imports/ui/components/note/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/note/styles.scss @@ -61,6 +61,22 @@ } } +.hint { + visibility: hidden; + position: absolute; + + @media (pointer: none) { + visibility: visible; + position: relative; + color: var(--color-gray); + font-size: var(--font-size-small); + font-style: italic; + padding-top: var(--sm-padding-x); + padding-left: var(--lg-padding-y); + text-align: left; + } +} + iframe { display: flex; flex-flow: column; @@ -70,4 +86,5 @@ iframe { overflow-x: hidden; overflow-y: auto; border-style: none; + border-bottom: 1px solid var(--color-gray-lightest); } diff --git a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx index 1f8c1c64b6bd5da60979f2fde4edee3564798fca..169d1dbf56a549b96d0355df2b1d40cac5f2833c 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx @@ -39,7 +39,8 @@ const intlMessages = defineMessages({ }, }); -const isFullscreen = () => document.fullscreenElement !== null; +const isFullscreen = () => !(document.fullscreenElement === null + || document.webkitFullscreenElement === null); // Edge const renderPresentationClose = () => { if (!shouldEnableSwapLayout() || isFullscreen()) return null; @@ -390,7 +391,14 @@ class PresentationArea extends Component { const { intl } = this.props; if (isFullscreen()) return null; - const full = () => this.refPresentationContainer.requestFullscreen(); + const full = () => { + const presentation = this.refPresentationContainer; + if (presentation.requestFullscreen) { + presentation.requestFullscreen(); + } else if (presentation.webkitRequestFullscreen) { // Edge + presentation.webkitRequestFullscreen(); + } + }; return ( <FullscreenButton diff --git a/bigbluebutton-html5/imports/ui/components/presentation/service.js b/bigbluebutton-html5/imports/ui/components/presentation/service.js index f8919fb25aa16fdc1567787818843f6f9460e38e..9edc8b4db71d4be79c59478c9388013c7b76c99e 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/service.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/service.js @@ -56,6 +56,76 @@ const getCurrentSlide = (podId) => { ); }; +const currentSlidHasContent = () => { + const currentSlide = getCurrentSlide('DEFAULT_PRESENTATION_POD'); + if (!currentSlide) return false; + + const { content } = currentSlide; + + return !!content.length; +}; + +const parseCurrentSlideContent = (yesValue, noValue, trueValue, falseValue) => { + const currentSlide = getCurrentSlide('DEFAULT_PRESENTATION_POD'); + const quickPollOptions = []; + if (!currentSlide) return quickPollOptions; + + const { content } = currentSlide; + + const pollRegex = /\n[^\s][.)]/g; + const optionsPoll = content.match(pollRegex) || []; + + const ynPollString = `(${yesValue}\\s*\\/\\s*${noValue})|(${noValue}\\s*\\/\\s*${yesValue})`; + const ynOptionsRegex = new RegExp(ynPollString, 'gi'); + const ynPoll = content.match(ynOptionsRegex) || []; + + const tfPollString = `(${trueValue}\\s*\\/\\s*${falseValue})|(${falseValue}\\s*\\/\\s*${trueValue})`; + const tgOptionsRegex = new RegExp(tfPollString, 'gi'); + const tfPoll = content.match(tgOptionsRegex) || []; + + optionsPoll.reduce((acc, currentValue) => { + const lastElement = acc[acc.length - 1]; + + if (!lastElement) { + acc.push({ + options: [currentValue], + }); + return acc; + } + + const { options } = lastElement; + + const lastOption = options[options.length - 1]; + + const isLastOptionInteger = !!parseInt(lastOption.charAt(1), 10); + const isCurrentValueInteger = !!parseInt(currentValue.charAt(1), 10); + + if (isLastOptionInteger === isCurrentValueInteger) { + if (currentValue.toLowerCase().charCodeAt(1) > lastOption.toLowerCase().charCodeAt(1)) { + options.push(currentValue); + } else { + acc.push({ + options: [currentValue], + }); + } + } else { + acc.push({ + options: [currentValue], + }); + } + + return acc; + }, []) + .filter(({ options }) => options.length > 1 && options.length < 7) + .forEach(poll => quickPollOptions.push({ type: `A-${poll.options.length}`, poll })); + + ynPoll.forEach(poll => quickPollOptions.push({ type: 'YN', poll })); + + tfPoll.forEach(poll => quickPollOptions.push({ type: 'TF', poll })); + + return { slideId: currentSlide.id, quickPollOptions }; +}; + const isPresenter = (podId) => { // a main presenter in the meeting always owns a default pod if (podId === 'DEFAULT_PRESENTATION_POD') { @@ -83,4 +153,6 @@ export default { isPresentationDownloadable, downloadPresentationUri, getMultiUserStatus, + currentSlidHasContent, + parseCurrentSlideContent, }; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/styles.scss b/bigbluebutton-html5/imports/ui/components/presentation/styles.scss index aa7a91c43ed793b8576fbb1fdfcc129acf3d5ce7..b9ae82a82bc74298eb90075c1efa1c032870656b 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/presentation/styles.scss @@ -77,4 +77,4 @@ clip: rect(0 0 0 0); height: 1px; width: 1px; margin: -1px; padding: 0; border: 0; -} \ No newline at end of file +} diff --git a/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss b/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss index f8a005c423d1b48449462bd0f11fa84341fa173c..94c2358b09c5657de5f26f96455216d72325ff87 100755 --- a/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss @@ -54,7 +54,7 @@ z-index: 1; [style~="--enableAnimation:1;"] & { - transition: .3s ease-in-out; + transition: .3s ease-in-out; } } } @@ -71,19 +71,21 @@ [style~="--enableAnimation:1;"] & { animation: pulse 1s infinite ease-in; } -} -[style~="--enableAnimation:0;"] .talking::before { - content: ''; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - background-color: var(--user-color); - border-radius: inherit; - box-shadow: 0px 0px 0px 4px var(--user-color); - opacity: .5; + &::before { + [style~="--enableAnimation:0;"] & { + content: ''; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: var(--user-color); + border-radius: inherit; + box-shadow: 0 0 0 4px var(--user-color); + opacity: .5; + } + } } @keyframes pulse { diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx index 42177bb19c6fdaffd55c08ee559a66110a0b75bb..a79bdf7d3321e6b2b9a75b1661079faa4b09f44f 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx @@ -226,7 +226,9 @@ class UserDropdown extends PureComponent { const enablePrivateChat = currentUser.isModerator ? allowedToChatPrivately : allowedToChatPrivately - && (!disablePrivChat || (disablePrivChat && hasPrivateChatBetweenUsers(currentUser, user))); + && (!(currentUser.isLocked && disablePrivChat) + || hasPrivateChatBetweenUsers(currentUser, user) + || user.isModerator); if (showNestedOptions) { if (allowedToChangeStatus) { diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx index 26229f2cf00ec0988b265413ceea44b0c2c19cad..88828591aa62357cf692aaf9266a8367d547e780 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx @@ -104,20 +104,12 @@ class UserOptions extends PureComponent { this.onActionsShow = this.onActionsShow.bind(this); this.onActionsHide = this.onActionsHide.bind(this); - this.alterMenu = this.alterMenu.bind(this); this.handleCreateBreakoutRoomClick = this.handleCreateBreakoutRoomClick.bind(this); this.onCreateBreakouts = this.onCreateBreakouts.bind(this); this.onInvitationUsers = this.onInvitationUsers.bind(this); this.renderMenuItems = this.renderMenuItems.bind(this); } - componentDidUpdate(prevProps) { - const { isMeetingMuted } = this.props; - if (prevProps.isMeetingMuted !== isMeetingMuted) { - this.alterMenu(); - } - } - onActionsShow() { this.setState({ isUserOptionsOpen: true, @@ -164,44 +156,6 @@ class UserOptions extends PureComponent { ); } - alterMenu() { - const { - intl, - isMeetingMuted, - toggleMuteAllUsers, - toggleMuteAllUsersExceptPresenter, - } = this.props; - - if (isMeetingMuted) { - const menuButton = ( - <DropdownListItem - key={_.uniqueId('list-item-')} - icon="unmute" - label={intl.formatMessage(intlMessages.unmuteAllLabel)} - description={intl.formatMessage(intlMessages.unmuteAllDesc)} - onClick={toggleMuteAllUsers} - /> - ); - this.menuItems.splice(1, 2, menuButton); - } else { - const muteMeetingButtons = [(<DropdownListItem - key={_.uniqueId('list-item-')} - icon="mute" - label={intl.formatMessage(intlMessages.muteAllLabel)} - description={intl.formatMessage(intlMessages.muteAllDesc)} - onClick={toggleMuteAllUsers} - />), (<DropdownListItem - key={_.uniqueId('list-item-')} - icon="mute" - label={intl.formatMessage(intlMessages.muteAllExceptPresenterLabel)} - description={intl.formatMessage(intlMessages.muteAllExceptPresenterDesc)} - onClick={toggleMuteAllUsersExceptPresenter} - />)]; - - this.menuItems.splice(1, 1, muteMeetingButtons[0], muteMeetingButtons[1]); - } - } - renderMenuItems() { const { intl, @@ -236,18 +190,20 @@ class UserOptions extends PureComponent { />), (<DropdownListItem key={this.muteAllId} - icon="mute" - label={intl.formatMessage(intlMessages.muteAllLabel)} - description={intl.formatMessage(intlMessages.muteAllDesc)} + icon={isMeetingMuted ? 'unmute' : 'mute'} + label={intl.formatMessage(intlMessages[isMeetingMuted ? 'unmuteAllLabel' : 'muteAllLabel'])} + description={intl.formatMessage(intlMessages[isMeetingMuted ? 'unmuteAllDesc' : 'muteAllDesc'])} onClick={toggleMuteAllUsers} />), - (<DropdownListItem - key={this.muteId} - icon="mute" - label={intl.formatMessage(intlMessages.muteAllExceptPresenterLabel)} - description={intl.formatMessage(intlMessages.muteAllExceptPresenterDesc)} - onClick={toggleMuteAllUsersExceptPresenter} - />), + (!isMeetingMuted ? ( + <DropdownListItem + key={this.muteId} + icon="mute" + label={intl.formatMessage(intlMessages.muteAllExceptPresenterLabel)} + description={intl.formatMessage(intlMessages.muteAllExceptPresenterDesc)} + onClick={toggleMuteAllUsersExceptPresenter} + />) : null + ), (<DropdownListItem key={this.lockId} icon="lock" @@ -278,10 +234,6 @@ class UserOptions extends PureComponent { : null), ]); - if (isMeetingMuted) { - this.alterMenu(); - } - return this.menuItems; } diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/fullscreen-button/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/fullscreen-button/component.jsx index ece3326cca6e1642c308aede1bf13787c4836fcd..a17757549808334eccb24f456cb6184a2ec6b46d 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/fullscreen-button/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/fullscreen-button/component.jsx @@ -29,7 +29,7 @@ const FullscreenButtonComponent = ({ }) => { const formattedLabel = intl.formatMessage( intlMessages.fullscreenButton, - ({ 0: elementName ? elementName.toLowerCase() : '' }), + ({ 0: elementName || '' }), ); return ( diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss index 8c25b9ed4a5c87da03764694dcce94b463886fa7..4e0943178bc91699f98a9f00789590b230de088a 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss @@ -30,6 +30,7 @@ .videoListItem { display: flex; + overflow: hidden; &.focused { grid-column: 1 / span 2; diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx index ea5c4dfbaeaa27e410a2b17f063012295498cfb9..8d208ddb7650dca7f59609327964a40064308414 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx @@ -109,7 +109,12 @@ class VideoListItem extends Component { renderFullscreenButton() { const { user } = this.props; const full = () => { - this.videoTag.requestFullscreen(); + const tag = this.videoTag; + if (tag.requestFullscreen) { + tag.requestFullscreen(); + } else if (tag.webkitRequestFullscreen) { // Edge + tag.webkitRequestFullscreen(); + } }; return <FullscreenButton handleFullscreen={full} elementName={user.name} />; } diff --git a/bigbluebutton-html5/imports/ui/stylesheets/variables/palette.scss b/bigbluebutton-html5/imports/ui/stylesheets/variables/palette.scss index e49d815bbe674512643e699e472ea0529fecaf48..0cfd6dd9f30ed2027f75cd0b563cb8a490f5da3a 100644 --- a/bigbluebutton-html5/imports/ui/stylesheets/variables/palette.scss +++ b/bigbluebutton-html5/imports/ui/stylesheets/variables/palette.scss @@ -7,6 +7,7 @@ --color-gray-dark: #06172A; --color-gray-light: #8B9AA8; --color-gray-lighter: #a7b3bd; + --color-gray-lightest: #d4d9df; --color-blue-light: #54a1f3; --color-blue-lighter: #92BCEA; diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 8e755abe398835b337f4b79eb542e312d093e52f..9f584a13f3bfaa83766807ce5ecd00de42bcaeaf 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -13,7 +13,7 @@ public: bbbServerVersion: 2.2-dev copyright: "©2019 BigBlueButton Inc." html5ClientBuild: HTML5_CLIENT_VERSION - helpLink: https://bigbluebutton.org/videos/ + helpLink: https://bigbluebutton.org/html5/ lockOnJoin: true basename: "/html5client" askForFeedbackOnLogout: false @@ -93,14 +93,14 @@ public: firefoxScreenshareSource: window cameraConstraints: width: - max: 640 + max: 320 height: - max: 480 + max: 240 enableScreensharing: false enableVideo: true enableVideoStats: false enableVideoMenu: true - enableListenOnly: false + enableListenOnly: true autoShareWebcam: false allowOutsideCommands: toggleRecording: false diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index acb687ee9012e030f52e690d1d3b8429be8af315..5495cffa4802bb45a65858bb2c2665c7f61c0638 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -21,6 +21,7 @@ "app.note.title": "Shared Notes", "app.note.label": "Note", "app.note.hideNoteLabel": "Hide note", + "app.note.tipLabel": "Press Esc to focus editor toolbar", "app.userList.usersTitle": "Users", "app.userList.participantsTitle": "Participants", "app.userList.messagesTitle": "Messages", @@ -119,6 +120,7 @@ "app.presentationUploder.removePresentationLabel": "Remove presentation", "app.presentationUploder.setAsCurrentPresentation": "Set presentation as current", "app.poll.pollPaneTitle": "Polling", + "app.poll.quickPollTitle": "Quick Poll", "app.poll.hidePollDesc": "Hides the poll menu pane", "app.poll.customPollInstruction": "To create a custom poll, select the button below and input your options.", "app.poll.quickPollInstruction": "Select an option below to start your poll.", @@ -130,7 +132,11 @@ "app.poll.closeLabel": "Close", "app.poll.ariaInputCount": "Input {0} of {1}", "app.poll.customPlaceholder": "Add poll option", + "app.poll.t": "True", + "app.poll.f": "False", "app.poll.tf": "True / False", + "app.poll.y": "Yes", + "app.poll.n": "No", "app.poll.yn": "Yes / No", "app.poll.a2": "A / B", "app.poll.a3": "A / B / C", diff --git a/record-and-playback/core/lib/recordandplayback/events_archiver.rb b/record-and-playback/core/lib/recordandplayback/events_archiver.rb index 484836279b29aa2bfbc72431f572a1a41dc1c546..c740d26e4baa49b81610cf1d3e74bad07405935d 100755 --- a/record-and-playback/core/lib/recordandplayback/events_archiver.rb +++ b/record-and-playback/core/lib/recordandplayback/events_archiver.rb @@ -328,8 +328,14 @@ module BigBlueButton # in the file, find the correct spot (it's usually no more than 1 or 2 off). # Make sure not to change the relative order of two events with the same timestamp. previous_event = recording.last_element_child + moved = 0 while previous_event.name == 'event' && previous_event['timestamp'].to_i > event['timestamp'].to_i previous_event = previous_event.previous_element + moved += 1 + end + if moved > 0 + BigBlueButton.logger.info("Reordered event timestamp=#{res[TIMESTAMP]} module=#{res[MODULE]} " \ + "eventname=#{res[EVENTNAME]} by #{moved} positions") end previous_event.add_next_sibling(event) diff --git a/record-and-playback/core/scripts/rap-archive-worker.rb b/record-and-playback/core/scripts/rap-archive-worker.rb index f8a27a9f099360c5f6f5b36bad058f90ca8183bd..f0fdd67f14cb9cbd138db57cfee4e26305b4573d 100755 --- a/record-and-playback/core/scripts/rap-archive-worker.rb +++ b/record-and-playback/core/scripts/rap-archive-worker.rb @@ -23,9 +23,6 @@ require 'rubygems' require 'yaml' require 'fileutils' -# Number of seconds to delay archiving (red5 race condition workaround) -ARCHIVE_DELAY_SECONDS = 120 - def archive_recorded_meetings(recording_dir) recorded_done_files = Dir.glob("#{recording_dir}/status/recorded/*.done") @@ -45,11 +42,6 @@ def archive_recorded_meetings(recording_dir) next end - if File.mtime(recorded_done) + ARCHIVE_DELAY_SECONDS > Time.now - BigBlueButton.logger.info("Temporarily skipping #{recorded_done_base} for Red5 race workaround") - next - end - archived_done = "#{recording_dir}/status/archived/#{recorded_done_base}.done" next if File.exists?(archived_done)