diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index 207091601ecdcda6be570cfe415db4d02d11ab6b..ed7a2222bbd15a22a0202518f367b1e330951c40 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -1,7 +1,6 @@ import React from 'react'; import cx from 'classnames'; import { styles } from './styles.scss'; -import EmojiSelect from './emoji-select/component'; import DesktopShare from './desktop-share/component'; import ActionsDropdown from './actions-dropdown/component'; import AudioControlsContainer from '../audio/audio-controls/container'; @@ -16,9 +15,6 @@ class ActionsBar extends React.PureComponent { handleShareScreen, handleUnshareScreen, isVideoBroadcasting, - emojiList, - emojiSelected, - handleEmojiChange, isUserModerator, recordSettingsList, toggleRecording, @@ -55,13 +51,12 @@ class ActionsBar extends React.PureComponent { handleCloseVideo={handleExitVideo} /> : null} - <EmojiSelect options={emojiList} selected={emojiSelected} onChange={handleEmojiChange} /> <DesktopShare {...{ -handleShareScreen, - handleUnshareScreen, - isVideoBroadcasting, - isUserPresenter, -}} + handleShareScreen, + handleUnshareScreen, + isVideoBroadcasting, + isUserPresenter, + }} /> </div> </div> diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx index ca2f85b1c1a5f6a6ba9688fdb452254e9f6375e8..b0a8150a32b7edf1dee47d9fba1ff83c47442f52 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx @@ -10,9 +10,6 @@ const ActionsBarContainer = props => <ActionsBar {...props} />; export default withTracker(() => ({ isUserPresenter: Service.isUserPresenter(), isUserModerator: Service.isUserModerator(), - emojiList: Service.getEmojiList(), - emojiSelected: Service.getEmoji(), - handleEmojiChange: Service.setEmoji, handleExitVideo: () => VideoService.exitVideo(), handleJoinVideo: () => VideoService.joinVideo(), handleShareScreen: () => shareScreen(), diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-select/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-select/component.jsx deleted file mode 100755 index 59931acdf3361ae3730f37d48e0211fd6cd77aec..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-select/component.jsx +++ /dev/null @@ -1,105 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, intlShape, injectIntl } from 'react-intl'; - -import Button from '/imports/ui/components/button/component'; -import Dropdown from '/imports/ui/components/dropdown/component'; -import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component'; -import DropdownContent from '/imports/ui/components/dropdown/content/component'; -import DropdownList from '/imports/ui/components/dropdown/list/component'; -import DropdownListItem from '/imports/ui/components/dropdown/list/item/component'; -import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component'; -import { styles } from '../styles'; - -const intlMessages = defineMessages({ - statusTriggerLabel: { - id: 'app.actionsBar.emojiMenu.statusTriggerLabel', - description: 'Emoji status button label', - }, - changeStatusLabel: { - id: 'app.actionsBar.changeStatusLabel', - description: 'Aria-label for emoji status button', - }, - currentStatusDesc: { - id: 'app.actionsBar.currentStatusDesc', - description: 'Aria description for status button', - }, -}); - -const propTypes = { - intl: intlShape.isRequired, - options: PropTypes.objectOf(PropTypes.string).isRequired, - selected: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, -}; - -const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts; -const OPEN_STATUS_AK = SHORTCUTS_CONFIG.openStatus.accesskey; - -const EmojiSelect = ({ - intl, - options, - selected, - onChange, -}) => { - const statuses = Object.keys(options); - const lastStatus = statuses.pop(); - - const statusLabel = intl.formatMessage(intlMessages.statusTriggerLabel); - - return ( - <Dropdown autoFocus> - <DropdownTrigger tabIndex={0}> - <Button - className={styles.button} - label={statusLabel} - aria-label={statusLabel} - aria-describedby="currentStatus" - icon={options[selected !== lastStatus ? selected : statuses[1]]} - ghost={false} - hideLabel - circle - size="lg" - color="primary" - onClick={() => null} - accessKey={OPEN_STATUS_AK} - > - <div id="currentStatus" hidden> - { intl.formatMessage(intlMessages.currentStatusDesc, { 0: selected }) } - </div> - </Button> - </DropdownTrigger> - <DropdownContent placement="top left"> - <DropdownList> - { - statuses.map(status => ( - <DropdownListItem - key={status} - className={status === selected ? styles.emojiSelected : null} - icon={options[status]} - label={intl.formatMessage({ id: `app.actionsBar.emojiMenu.${status}Label` })} - description={intl.formatMessage({ id: `app.actionsBar.emojiMenu.${status}Desc` })} - onClick={() => onChange(status)} - tabIndex={-1} - /> - )) - .concat( - <DropdownListSeparator key={-1} />, - <DropdownListItem - key={lastStatus} - icon={options[lastStatus]} - label={intl.formatMessage({ id: `app.actionsBar.emojiMenu.${lastStatus}Label` })} - description={intl.formatMessage({ id: `app.actionsBar.emojiMenu.${lastStatus}Desc` })} - onClick={() => onChange(lastStatus)} - tabIndex={-1} - />, - ) - } - </DropdownList> - </DropdownContent> - </Dropdown> - ); -}; - -EmojiSelect.propTypes = propTypes; -export default injectIntl(EmojiSelect); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js index 844b7f296634a5539db020ae6e945591f891c511..3eb815607df72101052fe4369cc746cb133dbbc1 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js @@ -1,14 +1,10 @@ import Auth from '/imports/ui/services/auth'; import Users from '/imports/api/users'; import { makeCall } from '/imports/ui/services/api'; -import { EMOJI_STATUSES } from '/imports/utils/statuses'; import Meetings from '/imports/api/meetings'; export default { isUserPresenter: () => Users.findOne({ userId: Auth.userID }).presenter, - getEmoji: () => Users.findOne({ userId: Auth.userID }).emoji, - setEmoji: status => makeCall('setEmojiStatus', Auth.userID, status), - getEmojiList: () => EMOJI_STATUSES, isUserModerator: () => Users.findOne({ userId: Auth.userID }).moderator, recordSettingsList: () => Meetings.findOne({ meetingId: Auth.meetingID }).recordProp, toggleRecording: () => makeCall('toggleRecording'), diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss index 5abb5ba57052801eddefe421d197374aad621f8e..4aa485dd4231b76a85cab34a97918feac94412f2 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss @@ -44,9 +44,3 @@ box-shadow: 0 2px 5px 0 rgb(0, 0, 0); } } - -.emojiSelected { - span, i { - color: $color-primary; - } -} diff --git a/bigbluebutton-html5/imports/ui/components/chat/styles.scss b/bigbluebutton-html5/imports/ui/components/chat/styles.scss index e33eb5c6fe36e53cbc9fdb064ad67e6520354eea..8889401573e40d5e2d2c7449052493ddaa670344 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/chat/styles.scss @@ -44,6 +44,7 @@ $background-active: darken($color-white, 5%); flex-direction: column; justify-content: space-around; overflow: hidden; + height: 100vh; } .header { diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx index bc6d9d243d32d8affcb811e435877ca9651f6b45..bd868773200d9f6cff8a63d8bff52a56c8ef6c5c 100644 --- a/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx @@ -73,15 +73,15 @@ class Dropdown extends Component { return nextState.isOpen ? screenreaderTrap.trap(this.dropdown) : screenreaderTrap.untrap(); } - componentDidUpdate(prevProps, prevState) { - if (this.state.isOpen && !prevState.isOpen) { - this.props.onShow(); - } + const { + onShow, + onHide, + } = this.props; - if (!this.state.isOpen && prevState.isOpen) { - this.props.onHide(); - } + if (this.state.isOpen && !prevState.isOpen) { onShow(); } + + if (!this.state.isOpen && prevState.isOpen) { onHide(); } } handleShow() { @@ -98,14 +98,17 @@ class Dropdown extends Component { }); } - handleWindowClick(event) { + handleWindowClick() { const triggerElement = findDOMNode(this.trigger); + const contentElement = findDOMNode(this.content); + const closeDropdown = this.props.isOpen && this.state.isOpen && triggerElement.contains(event.target); + const preventHide = this.props.isOpen && contentElement.contains(event.target) || !triggerElement; - if (!triggerElement) return; + if (closeDropdown) { + return this.props.onHide(); + } - if (!this.state.isOpen - || triggerElement === event.target - || triggerElement.contains(event.target)) { + if (contentElement && preventHide) { return; } diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx index 744331f4197d0bc0d77609c0ed57deebee15aed3..95496e1bad2d39b88a75fd830c92a45cb8405884 100755 --- a/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx @@ -6,7 +6,6 @@ import { styles } from './styles'; import ListItem from './item/component'; import ListSeparator from './separator/component'; import ListTitle from './title/component'; -import UserActions from '../../user-list/user-list-content/user-participants/user-list-item/user-action/component'; const propTypes = { /* We should recheck this proptype, sometimes we need to create an container and send to dropdown, @@ -15,8 +14,7 @@ const propTypes = { children: PropTypes.arrayOf((propValue, key, componentName, location, propFullName) => { if (propValue[key].type !== ListItem && propValue[key].type !== ListSeparator && - propValue[key].type !== ListTitle && - propValue[key].type !== UserActions) { + propValue[key].type !== ListTitle) { return new Error(`Invalid prop \`${propFullName}\` supplied to` + ` \`${componentName}\`. Validation failed.`); } diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx index cb7f49b57ee9b11d10c8c1431fe3ce57161e4826..41c158f9dadd9f22fd97153df0ccdcec7119ab51 100644 --- a/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx @@ -26,11 +26,12 @@ export default class DropdownListItem extends Component { } renderDefault() { - const { icon, label } = this.props; + const { icon, label, iconRight } = this.props; return [ (icon ? <Icon iconName={icon} key="icon" className={styles.itemIcon} /> : null), (<span className={styles.itemLabel} key="label">{label}</span>), + (iconRight ? <Icon iconName={iconRight} key="iconRight" className={styles.iconRight} /> : null), ]; } diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss b/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss index 991fc52f73855432ee8a0f46c0e3df155130b888..06f65f09c8bbb9d8525a60b2f68aeb60cc6b75fd 100755 --- a/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss @@ -1,5 +1,8 @@ @import "/imports/ui/stylesheets/variables/_all"; +$more-icon-font-size: 12px; +$more-icon-line-height: 16px; + %list { list-style: none; font-size: $font-size-base; @@ -107,6 +110,7 @@ border-radius: 0.2rem; } + .iconRight, .itemIcon, .itemLabel { color: inherit; @@ -118,12 +122,20 @@ } } +.iconRight, .itemIcon { margin-right: ($line-height-computed / 2); color: $color-text; flex: 0 0; } +.iconRight { + margin-right: -$indicator-padding-left; + margin-left: $sm-padding-x; + font-size: $more-icon-font-size; + line-height: $more-icon-line-height; +} + .itemLabel { color: $color-gray-dark; font-size: 90%; diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx index 66d9ff5458ca768fe6520dbc0a5eb6cc8499eb0e..50f3859de848779befcd699a07df2e32c40a09ca 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx @@ -73,6 +73,14 @@ const intlMessages = defineMessages({ id: 'app.navBar.settingsDropdown.hotkeysDesc', description: 'Describes hotkeys option', }, + helpLabel: { + id: 'app.navBar.settingsDropdown.helpLabel', + description: 'Help options label', + }, + helpDesc: { + id: 'app.navBar.settingsDropdown.helpDesc', + description: 'Describes help option', + }, }); const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts; @@ -93,8 +101,9 @@ class SettingsDropdown extends Component { componentWillMount() { const { intl, mountModal, isAndroid } = this.props; const { fullscreenLabel, fullscreenDesc, fullscreenIcon } = this.checkFullscreen(this.props); + const { showHelpButton: helpButton } = Meteor.settings.public.app; - this.menuItems = [(<DropdownListItem + this.menuItems =_.compact( [(<DropdownListItem key={_.uniqueId('list-item-')} icon={fullscreenIcon} label={fullscreenLabel} @@ -115,9 +124,17 @@ class SettingsDropdown extends Component { description={intl.formatMessage(intlMessages.aboutDesc)} onClick={() => mountModal(<AboutContainer />)} />), + !helpButton ? null : (<DropdownListItem key={_.uniqueId('list-item-')} - icon="about" + icon="help" + label={intl.formatMessage(intlMessages.helpLabel)} + description={intl.formatMessage(intlMessages.helpDesc)} + onClick={() => window.open('https://bigbluebutton.org/videos/')} + />), + (<DropdownListItem + key={_.uniqueId('list-item-')} + icon="shortcuts" label={intl.formatMessage(intlMessages.hotkeysLabel)} description={intl.formatMessage(intlMessages.hotkeysDesc)} onClick={() => mountModal(<ShortcutHelpComponent />)} @@ -130,12 +147,14 @@ class SettingsDropdown extends Component { description={intl.formatMessage(intlMessages.leaveSessionDesc)} onClick={() => mountModal(<LogoutConfirmationContainer />)} />), - ]; + ]) // Removes fullscreen button if not on Android if (!isAndroid) { this.menuItems.shift(); } + + } componentWillReceiveProps(nextProps) { diff --git a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx index 27c04e3ac70ebb9be7aa233fe45c24642f181e79..5937ce0dfec54835965976b038eada7acea5bc3c 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx @@ -62,6 +62,9 @@ class UserList extends Component { isPublicChat, roving, CustomLogoUrl, + handleEmojiChange, + getEmojiList, + getEmoji, } = this.props; return ( @@ -91,6 +94,9 @@ class UserList extends Component { isMeetingLocked, isPublicChat, roving, + handleEmojiChange, + getEmojiList, + getEmoji, } } />} diff --git a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx index f30d12964cec76792ee4e045df61b718339d6a3e..8ab9a238ebd1127c544d45da62d1ee26593677e3 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx @@ -46,4 +46,7 @@ export default withTracker(({ chatID, compact }) => ({ roving: Service.roving, CustomLogoUrl: Service.getCustomLogoUrl(), compact, + handleEmojiChange: Service.setEmojiStatus, + getEmojiList: Service.getEmojiList(), + getEmoji: Service.getEmoji(), }))(UserListContainer); diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js index 11290c48f1b8bcd3a54386695423814089627525..04c64e86cdbba28dab6f412840e55ab11fef7987 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/service.js +++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js @@ -277,6 +277,8 @@ const getAvailableActions = (currentUser, user, router, isBreakoutRoom) => { && user.isModerator && !isDialInUser; + const allowedToChangeStatus = user.isCurrent; + return { allowedToChatPrivately, allowedToMuteAudio, @@ -286,6 +288,7 @@ const getAvailableActions = (currentUser, user, router, isBreakoutRoom) => { allowedToSetPresenter, allowedToPromote, allowedToDemote, + allowedToChangeStatus, }; }; @@ -318,7 +321,13 @@ const isMeetingLocked = (id) => { return isLocked; }; -const setEmojiStatus = (userId) => { makeCall('setEmojiStatus', userId, 'none'); }; +const setEmojiStatus = (data) => { + const statusAvailable = (Object.keys(EMOJI_STATUSES).includes(data)); + + return statusAvailable + ? makeCall('setEmojiStatus', Auth.userID, data) + : makeCall('setEmojiStatus', data, 'none'); +}; const assignPresenter = (userId) => { makeCall('assignPresenter', userId); }; @@ -409,4 +418,6 @@ export default { roving, setCustomLogoUrl, getCustomLogoUrl, + getEmojiList: () => EMOJI_STATUSES, + getEmoji: () => Users.findOne({ userId: Auth.userID }).emoji, }; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx index 74025e823c8a1c37b385f0cc03fe2be20d3e8bd8..37e2b7931f2ecca207454ac4991d77c5e10df4f3 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { styles } from './styles'; import UserParticipants from './user-participants/component'; @@ -34,8 +34,31 @@ const defaultProps = { meeting: {}, }; -class UserContent extends Component { +class UserContent extends React.PureComponent { render() { + const { + users, + compact, + intl, + currentUser, + meeting, + isBreakoutRoom, + setEmojiStatus, + assignPresenter, + removeUser, + toggleVoice, + changeRole, + getAvailableActions, + normalizeEmojiName, + isMeetingLocked, + roving, + handleEmojiChange, + getEmojiList, + getEmoji, + isPublicChat, + openChats, + } = this.props; + return ( <div data-test="userListContent" @@ -43,28 +66,35 @@ class UserContent extends Component { role="complementary" > <UserMessages - isPublicChat={this.props.isPublicChat} - openChats={this.props.openChats} - compact={this.props.compact} - intl={this.props.intl} - roving={this.props.roving} + {...{ + isPublicChat, + openChats, + compact, + intl, + roving, + }} /> <UserParticipants - users={this.props.users} - compact={this.props.compact} - intl={this.props.intl} - currentUser={this.props.currentUser} - meeting={this.props.meeting} - isBreakoutRoom={this.props.isBreakoutRoom} - setEmojiStatus={this.props.setEmojiStatus} - assignPresenter={this.props.assignPresenter} - removeUser={this.props.removeUser} - toggleVoice={this.props.toggleVoice} - changeRole={this.props.changeRole} - getAvailableActions={this.props.getAvailableActions} - normalizeEmojiName={this.props.normalizeEmojiName} - isMeetingLocked={this.props.isMeetingLocked} - roving={this.props.roving} + {...{ + users, + compact, + intl, + currentUser, + meeting, + isBreakoutRoom, + setEmojiStatus, + assignPresenter, + removeUser, + toggleVoice, + changeRole, + getAvailableActions, + normalizeEmojiName, + isMeetingLocked, + roving, + handleEmojiChange, + getEmojiList, + getEmoji, + }} /> </div> ); diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx index df0745e206c4406bf54a4131d894188f7e5ee2a2..c772dc782dd1e8779d9c55f049b2ca3f604d01ec 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import { TransitionGroup, CSSTransition } from 'react-transition-group'; -import PropTypes from 'prop-types'; import { defineMessages } from 'react-intl'; +import PropTypes from 'prop-types'; import cx from 'classnames'; import { styles } from '/imports/ui/components/user-list/user-list-content/styles'; import UserListItem from './user-list-item/component'; @@ -48,38 +48,6 @@ const intlMessages = defineMessages({ id: 'app.userList.usersTitle', description: 'Title for the Header', }, - ChatLabel: { - id: 'app.userList.menu.chat.label', - description: 'Save the changes and close the settings menu', - }, - ClearStatusLabel: { - id: 'app.userList.menu.clearStatus.label', - description: 'Clear the emoji status of this user', - }, - MakePresenterLabel: { - id: 'app.userList.menu.makePresenter.label', - description: 'Set this user to be the presenter in this meeting', - }, - RemoveUserLabel: { - id: 'app.userList.menu.removeUser.label', - description: 'Forcefully remove this user from the meeting', - }, - MuteUserAudioLabel: { - id: 'app.userList.menu.muteUserAudio.label', - description: 'Forcefully mute this user', - }, - UnmuteUserAudioLabel: { - id: 'app.userList.menu.unmuteUserAudio.label', - description: 'Forcefully unmute this user', - }, - PromoteUserLabel: { - id: 'app.userList.menu.promoteUser.label', - description: 'Forcefully promote this viewer to a moderator', - }, - DemoteUserLabel: { - id: 'app.userList.menu.demoteUser.label', - description: 'Forcefully demote this moderator to a viewer', - }, }); class UserParticipants extends Component { @@ -136,58 +104,16 @@ class UserParticipants extends Component { normalizeEmojiName, isMeetingLocked, users, - intl, changeRole, assignPresenter, setEmojiStatus, removeUser, toggleVoice, + handleEmojiChange, + getEmojiList, + getEmoji, } = this.props; - const userActions = - { - openChat: { - label: () => intl.formatMessage(intlMessages.ChatLabel), - handler: (router, user) => router.push(`/users/chat/${user.id}`), - icon: 'chat', - }, - clearStatus: { - label: () => intl.formatMessage(intlMessages.ClearStatusLabel), - handler: user => setEmojiStatus(user.id, 'none'), - icon: 'clear_status', - }, - setPresenter: { - label: () => intl.formatMessage(intlMessages.MakePresenterLabel), - handler: user => assignPresenter(user.id), - icon: 'presentation', - }, - remove: { - label: user => intl.formatMessage(intlMessages.RemoveUserLabel, { 0: user.name }), - handler: user => removeUser(user.id), - icon: 'circle_close', - }, - mute: { - label: () => intl.formatMessage(intlMessages.MuteUserAudioLabel), - handler: user => toggleVoice(user.id), - icon: 'mute', - }, - unmute: { - label: () => intl.formatMessage(intlMessages.UnmuteUserAudioLabel), - handler: user => toggleVoice(user.id), - icon: 'unmute', - }, - promote: { - label: () => intl.formatMessage(intlMessages.PromoteUserLabel), - handler: user => changeRole(user.id, 'MODERATOR'), - icon: 'promote', - }, - demote: { - label: () => intl.formatMessage(intlMessages.DemoteUserLabel), - handler: user => changeRole(user.id, 'VIEWER'), - icon: 'user', - }, - }; - let index = -1; return users.map(user => ( @@ -203,15 +129,24 @@ class UserParticipants extends Component { > <div ref={(node) => { this.userRefs[index += 1] = node; }}> <UserListItem - compact={compact} - isBreakoutRoom={isBreakoutRoom} - user={user} - currentUser={currentUser} - userActions={userActions} - meeting={meeting} - getAvailableActions={getAvailableActions} - normalizeEmojiName={normalizeEmojiName} - isMeetingLocked={isMeetingLocked} + {...{ + user, + currentUser, + compact, + isBreakoutRoom, + meeting, + getAvailableActions, + normalizeEmojiName, + isMeetingLocked, + handleEmojiChange, + getEmojiList, + getEmoji, + setEmojiStatus, + assignPresenter, + removeUser, + toggleVoice, + changeRole, + }} getScrollContainerRef={this.getScrollContainerRef} /> </div> @@ -220,9 +155,7 @@ class UserParticipants extends Component { } focusUserItem(index) { - if (!this.userRefs[index]) { - return; - } + if (!this.userRefs[index]) return; this.userRefs[index].firstChild.focus(); } diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx index 26d2c09dc9acf5647b53ce5f085359ab838b4be4..e3e4fc585745629138d818f7b08b6c898dabe522 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx @@ -2,9 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { withRouter } from 'react-router'; import { injectIntl } from 'react-intl'; -import _ from 'lodash'; -import UserListContent from './user-list-content/component'; -import UserAction from './user-action/component'; +import UserDropdown from './user-dropdown/component'; const propTypes = { user: PropTypes.shape({ @@ -23,7 +21,6 @@ const propTypes = { intl: PropTypes.shape({ formatMessage: PropTypes.func.isRequired, }).isRequired, - userActions: PropTypes.shape({}).isRequired, router: PropTypes.shape({}).isRequired, isBreakoutRoom: PropTypes.bool, getAvailableActions: PropTypes.func.isRequired, @@ -38,64 +35,6 @@ const defaultProps = { }; class UserListItem extends Component { - static createAction(action, ...options) { - return ( - <UserAction - key={_.uniqueId('action-item-')} - icon={action.icon} - label={action.label(...options)} - handler={action.handler} - options={[...options]} - /> - ); - } - - getUsersActions() { - const { - currentUser, - user, - userActions, - router, - isBreakoutRoom, - getAvailableActions, - } = this.props; - - const { - openChat, - clearStatus, - setPresenter, - remove, - mute, - unmute, - promote, - demote, - } = userActions; - - const actions = getAvailableActions(currentUser, user, router, isBreakoutRoom); - - const { - allowedToChatPrivately, - allowedToMuteAudio, - allowedToUnmuteAudio, - allowedToResetStatus, - allowedToRemove, - allowedToSetPresenter, - allowedToPromote, - allowedToDemote, - } = actions; - - return _.compact([ - (allowedToChatPrivately ? UserListItem.createAction(openChat, router, user) : null), - (allowedToMuteAudio ? UserListItem.createAction(mute, user) : null), - (allowedToUnmuteAudio ? UserListItem.createAction(unmute, user) : null), - (allowedToResetStatus ? UserListItem.createAction(clearStatus, user) : null), - (allowedToSetPresenter ? UserListItem.createAction(setPresenter, user) : null), - (allowedToRemove ? UserListItem.createAction(remove, user) : null), - (allowedToPromote ? UserListItem.createAction(promote, user) : null), - (allowedToDemote ? UserListItem.createAction(demote, user) : null), - ]); - } - render() { const { compact, @@ -105,19 +44,42 @@ class UserListItem extends Component { isMeetingLocked, normalizeEmojiName, getScrollContainerRef, + assignPresenter, + removeUser, + toggleVoice, + changeRole, + setEmojiStatus, + currentUser, + router, + isBreakoutRoom, + getAvailableActions, + handleEmojiChange, + getEmojiList, + getEmoji, } = this.props; - const actions = this.getUsersActions(); - - const contents = (<UserListContent - compact={compact} - user={user} - intl={intl} - normalizeEmojiName={normalizeEmojiName} - actions={actions} - meeting={meeting} - isMeetingLocked={isMeetingLocked} - getScrollContainerRef={getScrollContainerRef} + const contents = (<UserDropdown + {...{ + compact, + user, + intl, + normalizeEmojiName, + meeting, + isMeetingLocked, + getScrollContainerRef, + assignPresenter, + removeUser, + toggleVoice, + changeRole, + setEmojiStatus, + currentUser, + router, + isBreakoutRoom, + getAvailableActions, + handleEmojiChange, + getEmojiList, + getEmoji, + }} />); return contents; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-action/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-action/component.jsx deleted file mode 100755 index 955f5a5468184548c4848947f14793c7d86f621e..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-action/component.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import DropdownListItem from '/imports/ui/components/dropdown/list/item/component'; - -const propTypes = { - icon: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - handler: PropTypes.func.isRequired, - options: PropTypes.arrayOf(PropTypes.shape({})).isRequired, -}; - -export default class UserActions extends React.PureComponent { - render() { - const { - key, icon, label, handler, options, - } = this.props; - - return ( - <DropdownListItem - key={key} - icon={icon} - label={label} - defaultMessage={label} - onClick={() => handler.call(this, ...options)} - /> - ); - } -} - -UserActions.propTypes = propTypes; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx similarity index 52% rename from bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/component.jsx rename to bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx index 1ba403fe4b4ce88dd032aa60ee87f614431aa4b5..f3c5a1c805d5bd2277abba7a60505d04b2b220e1 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx @@ -8,8 +8,9 @@ import Dropdown from '/imports/ui/components/dropdown/component'; import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component'; import DropdownContent from '/imports/ui/components/dropdown/content/component'; import DropdownList from '/imports/ui/components/dropdown/list/component'; +import DropdownListItem from '/imports/ui/components/dropdown/list/item/component'; import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component'; -import DropdownListTitle from '/imports/ui/components/dropdown/list/title/component'; +import _ from 'lodash'; import { styles } from './styles'; import UserName from './../user-name/component'; import UserIcons from './../user-icons/component'; @@ -39,6 +40,46 @@ const messages = defineMessages({ id: 'app.userList.userAriaLabel', description: 'aria label for each user in the userlist', }, + statusTriggerLabel: { + id: 'app.actionsBar.emojiMenu.statusTriggerLabel', + description: 'label for option to show emoji menu', + }, + backTriggerLabel: { + id: 'app.audio.backLabel', + description: 'label for option to hide emoji menu', + }, + ChatLabel: { + id: 'app.userList.menu.chat.label', + description: 'Save the changes and close the settings menu', + }, + ClearStatusLabel: { + id: 'app.userList.menu.clearStatus.label', + description: 'Clear the emoji status of this user', + }, + MakePresenterLabel: { + id: 'app.userList.menu.makePresenter.label', + description: 'Set this user to be the presenter in this meeting', + }, + RemoveUserLabel: { + id: 'app.userList.menu.removeUser.label', + description: 'Forcefully remove this user from the meeting', + }, + MuteUserAudioLabel: { + id: 'app.userList.menu.muteUserAudio.label', + description: 'Forcefully mute this user', + }, + UnmuteUserAudioLabel: { + id: 'app.userList.menu.unmuteUserAudio.label', + description: 'Forcefully unmute this user', + }, + PromoteUserLabel: { + id: 'app.userList.menu.promoteUser.label', + description: 'Forcefully promote this viewer to a moderator', + }, + DemoteUserLabel: { + id: 'app.userList.menu.demoteUser.label', + description: 'Forcefully demote this moderator to a viewer', + }, }); const propTypes = { @@ -48,14 +89,12 @@ const propTypes = { formatMessage: PropTypes.func.isRequired, }).isRequired, normalizeEmojiName: PropTypes.func.isRequired, - actions: PropTypes.arrayOf(PropTypes.shape({})).isRequired, meeting: PropTypes.shape({}).isRequired, isMeetingLocked: PropTypes.func.isRequired, getScrollContainerRef: PropTypes.func.isRequired, }; - -class UserListContent extends Component { +class UserDropdown extends Component { /** * Return true if the content fit on the screen, false otherwise. * @@ -75,6 +114,7 @@ class UserListContent extends Component { dropdownOffset: 0, dropdownDirection: 'top', dropdownVisible: false, + showNestedOptions: false, }; this.handleScroll = this.handleScroll.bind(this); @@ -82,6 +122,8 @@ class UserListContent extends Component { this.onActionsHide = this.onActionsHide.bind(this); this.getDropdownMenuParent = this.getDropdownMenuParent.bind(this); this.renderUserAvatar = this.renderUserAvatar.bind(this); + this.resetMenuState = this.resetMenuState.bind(this); + this.makeDropdownItem = this.makeDropdownItem.bind(this); } componentWillMount() { @@ -89,30 +131,199 @@ class UserListContent extends Component { this.seperator = _.uniqueId('action-separator-'); } - componentDidUpdate() { + componentDidUpdate(prevProps, prevState) { + if (!this.state.isActionsOpen && this.state.showNestedOptions) { + return this.resetMenuState(); + } + this.checkDropdownDirection(); } - onActionsShow() { - const dropdown = this.getDropdownMenuParent(); - const scrollContainer = this.props.getScrollContainerRef(); - const dropdownTrigger = dropdown.children[0]; - - const list = findDOMNode(this.list); - const children = [].slice.call(list.children); - children.find(child => child.getAttribute('role') === 'menuitem').focus(); + makeDropdownItem(key, label, onClick, icon = null, iconRight = null) { + return ( + <DropdownListItem + key={key} + label={label} + onClick={onClick} + icon={icon} + iconRight={iconRight} + className={key === this.props.getEmoji ? styles.emojiSelected : null} + /> + ); + } - this.setState({ - isActionsOpen: true, - dropdownVisible: false, - dropdownOffset: dropdownTrigger.offsetTop - scrollContainer.scrollTop, + resetMenuState() { + return this.setState({ + isActionsOpen: false, + dropdownOffset: 0, dropdownDirection: 'top', + dropdownVisible: false, + showNestedOptions: false, }); + } + + getUsersActions() { + const { + intl, + currentUser, + user, + router, + isBreakoutRoom, + getAvailableActions, + handleEmojiChange, + getEmojiList, + setEmojiStatus, + assignPresenter, + removeUser, + toggleVoice, + changeRole, + } = this.props; + + const actionPermissions = getAvailableActions(currentUser, user, router, isBreakoutRoom); + const actions = []; + + const { + allowedToChatPrivately, + allowedToMuteAudio, + allowedToUnmuteAudio, + allowedToResetStatus, + allowedToRemove, + allowedToSetPresenter, + allowedToPromote, + allowedToDemote, + allowedToChangeStatus, + } = actionPermissions; + + if (this.state.showNestedOptions) { + if (allowedToChangeStatus) { + actions.push(this.makeDropdownItem( + 'back', + intl.formatMessage(messages.backTriggerLabel), + () => this.setState({ showNestedOptions: false, isActionsOpen: true }), + 'left_arrow', + )); + } + + actions.push(<DropdownListSeparator key={_.uniqueId('list-separator-')} />); + + const statuses = Object.keys(getEmojiList); + statuses.map(status => actions.push(this.makeDropdownItem( + status, + intl.formatMessage({ id: `app.actionsBar.emojiMenu.${status}Label` }), + () => { handleEmojiChange(status); this.resetMenuState(); }, + getEmojiList[status], + ))); - scrollContainer.addEventListener('scroll', this.handleScroll, false); + return actions; + } + + if (allowedToChangeStatus) { + actions.push(this.makeDropdownItem( + 'setstatus', + intl.formatMessage(messages.statusTriggerLabel), + () => this.setState({ showNestedOptions: true, isActionsOpen: true }), + 'user', + 'right_arrow', + )); + } + + if (allowedToChatPrivately) { + actions.push(this.makeDropdownItem( + 'openChat', + intl.formatMessage(messages.ChatLabel), + () => this.onActionsHide(router.push(`/users/chat/${user.id}`)), + 'chat', + )); + } + + if (allowedToMuteAudio) { + actions.push(this.makeDropdownItem( + 'mute', + intl.formatMessage(messages.MuteUserAudioLabel), + () => this.onActionsHide(toggleVoice(user.id)), + 'mute', + )); + } + + if (allowedToUnmuteAudio) { + actions.push(this.makeDropdownItem( + 'unmute', + intl.formatMessage(messages.UnmuteUserAudioLabel), + () => this.onActionsHide(toggleVoice(user.id)), + 'unmute', + )); + } + + if (allowedToResetStatus && user.emoji.status !== 'none') { + actions.push(this.makeDropdownItem( + 'clearStatus', + intl.formatMessage(messages.ClearStatusLabel), + () => this.onActionsHide(setEmojiStatus(user.id, 'none')), + 'clear_status', + )); + } + + if (allowedToSetPresenter) { + actions.push(this.makeDropdownItem( + 'setPresenter', + intl.formatMessage(messages.MakePresenterLabel), + () => this.onActionsHide(assignPresenter(user.id)), + 'presentation', + )); + } + + if (allowedToRemove) { + actions.push(this.makeDropdownItem( + 'remove', + intl.formatMessage(messages.RemoveUserLabel, { 0: user.name }), + () => this.onActionsHide(removeUser(user.id)), + 'circle_close', + )); + } + + if (allowedToPromote) { + actions.push(this.makeDropdownItem( + 'promote', + intl.formatMessage(messages.PromoteUserLabel), + () => this.onActionsHide(changeRole(user.id, 'MODERATOR')), + 'promote', + )); + } + + if (allowedToDemote) { + actions.push(this.makeDropdownItem( + 'demote', + intl.formatMessage(messages.DemoteUserLabel), + () => this.onActionsHide(changeRole(user.id, 'VIEWER')), + 'user', + )); + } + + return actions; + } + + onActionsShow() { + const dropdown = this.getDropdownMenuParent(); + const scrollContainer = this.props.getScrollContainerRef(); + + if (dropdown && scrollContainer) { + const dropdownTrigger = dropdown.children[0]; + const list = findDOMNode(this.list); + const children = [].slice.call(list.children); + children.find(child => child.getAttribute('role') === 'menuitem').focus(); + + this.setState({ + isActionsOpen: true, + dropdownVisible: false, + dropdownOffset: dropdownTrigger.offsetTop - scrollContainer.scrollTop, + dropdownDirection: 'top', + }); + + scrollContainer.addEventListener('scroll', this.handleScroll, false); + } } - onActionsHide() { + onActionsHide(callback) { this.setState({ isActionsOpen: false, dropdownVisible: false, @@ -120,6 +331,10 @@ class UserListContent extends Component { const scrollContainer = this.props.getScrollContainerRef(); scrollContainer.removeEventListener('scroll', this.handleScroll, false); + + if (callback) { + return callback; + } } getDropdownMenuParent() { @@ -127,9 +342,7 @@ class UserListContent extends Component { } handleScroll() { - this.setState({ - isActionsOpen: false, - }); + this.setState({ isActionsOpen: false }); } /** @@ -148,7 +361,7 @@ class UserListContent extends Component { }; const isDropdownVisible = - UserListContent.checkIfDropdownIsVisible( + UserDropdown.checkIfDropdownIsVisible( dropdownContent.offsetTop, dropdownContent.offsetHeight, ); @@ -211,7 +424,6 @@ class UserListContent extends Component { compact, user, intl, - actions, isMeetingLocked, meeting, } = this.props; @@ -223,6 +435,8 @@ class UserListContent extends Component { dropdownOffset, } = this.state; + const actions = this.getUsersActions(); + const userItemContentsStyle = {}; userItemContentsStyle[styles.userItemContentsCompact] = compact; @@ -255,25 +469,27 @@ class UserListContent extends Component { { this.renderUserAvatar() } </div> {<UserName - user={user} - compact={compact} - intl={intl} - meeting={meeting} - isMeetingLocked={isMeetingLocked} - userAriaLabel={userAriaLabel} - isActionsOpen={isActionsOpen} + {...{ + user, + compact, + intl, + meeting, + isMeetingLocked, + userAriaLabel, + isActionsOpen, + }} />} {<UserIcons - user={user} - compact={compact} + {...{ + user, + compact, + }} />} </div> </div> ); - if (!actions.length) { - return contents; - } + if (!actions.length) return contents; return ( <Dropdown @@ -298,24 +514,12 @@ class UserListContent extends Component { className={styles.dropdownContent} placement={`right ${dropdownDirection}`} > - <DropdownList ref={(ref) => { this.list = ref; }} getDropdownMenuParent={this.getDropdownMenuParent} onActionsHide={this.onActionsHide} > - { - [ - ( - <DropdownListTitle - description={intl.formatMessage(messages.menuTitleContext)} - key={this.title} - > - {user.name} - </DropdownListTitle>), - (<DropdownListSeparator key={this.seperator} />), - ].concat(actions) - } + {actions} </DropdownList> </DropdownContent> </Dropdown> @@ -323,5 +527,5 @@ class UserListContent extends Component { } } -UserListContent.propTypes = propTypes; -export default UserListContent; +UserDropdown.propTypes = propTypes; +export default UserDropdown; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/styles.scss similarity index 96% rename from bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/styles.scss rename to bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/styles.scss index 441d4377ac6b264410884aa1bf0738d5fdb6036c..e6e8b750e51a95e4d044ad7f847b3f4069ed25d7 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/styles.scss @@ -103,3 +103,9 @@ max-width: 100%; overflow: visible; } + +.emojiSelected { + span, i { + color: $color-primary; + } +} diff --git a/bigbluebutton-html5/imports/utils/statuses.js b/bigbluebutton-html5/imports/utils/statuses.js index 9aa785f63997e224beb0e8ab4492bdfbc6833410..848c521c4788f787de3e1c763de21cc659dfe31b 100644 --- a/bigbluebutton-html5/imports/utils/statuses.js +++ b/bigbluebutton-html5/imports/utils/statuses.js @@ -9,7 +9,6 @@ export const EMOJI_STATUSES = { applause: 'applause', thumbsUp: 'thumbs_up', thumbsDown: 'thumbs_down', - none: 'clear_status', }; export default { EMOJI_STATUSES }; diff --git a/bigbluebutton-html5/private/config/settings-development.json b/bigbluebutton-html5/private/config/settings-development.json index 2b4df6c42a431e63d05fedd0baadd61744ad98cb..75dc9e6c49691938178383679d13feca9edd3153 100755 --- a/bigbluebutton-html5/private/config/settings-development.json +++ b/bigbluebutton-html5/private/config/settings-development.json @@ -67,7 +67,8 @@ "allowHTML5Moderator": true, "allowModeratorToUnmuteAudio": true, "httpsConnection": false, - "connectionTimeout": 60000 + "connectionTimeout": 60000, + "showHelpButton": true }, "kurento": { diff --git a/bigbluebutton-html5/private/config/settings-production.json b/bigbluebutton-html5/private/config/settings-production.json index f9feb3d4356355f226b870bc517be56031bd483d..e5ed8b1f837f9a384f566226dac4f85155236dc6 100755 --- a/bigbluebutton-html5/private/config/settings-production.json +++ b/bigbluebutton-html5/private/config/settings-production.json @@ -67,7 +67,8 @@ "allowHTML5Moderator": true, "allowModeratorToUnmuteAudio": true, "httpsConnection": true, - "connectionTimeout": 10000 + "connectionTimeout": 10000, + "showHelpButton": true }, "kurento": { diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 38f308196217e82dbcdf6e433f04659feb83a4d5..8091d5e7e3c95a58943383af28a3d456c01b502e 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -97,6 +97,8 @@ "app.navBar.settingsDropdown.exitFullscreenDesc": "Exit fullscreen mode", "app.navBar.settingsDropdown.hotkeysLabel": "Hotkeys", "app.navBar.settingsDropdown.hotkeysDesc": "Listing of available hotkeys", + "app.navBar.settingsDropdown.helpLabel": "Help", + "app.navBar.settingsDropdown.helpDesc": "Links user to video tutorials", "app.navBar.userListToggleBtnLabel": "User List Toggle", "app.navBar.toggleUserList.ariaLabel": "Users and Messages Toggle", "app.navBar.toggleUserList.newMessages": "with new message notification", @@ -200,7 +202,7 @@ "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Stop sharing your screen with", "app.actionsBar.actionsDropdown.startRecording": "Start recording", "app.actionsBar.actionsDropdown.stopRecording": "Stop recording", - "app.actionsBar.emojiMenu.statusTriggerLabel": "Set a Status", + "app.actionsBar.emojiMenu.statusTriggerLabel": "Set Status", "app.actionsBar.emojiMenu.awayLabel": "Away", "app.actionsBar.emojiMenu.awayDesc": "Change your status to away", "app.actionsBar.emojiMenu.raiseHandLabel": "Raise",