Merge pull request #5839 from KDSBrowne/5782-move-set-status

Move set status menu to user item dropdown
This commit is contained in:
Anton Georgiev 2018-08-13 15:00:04 -04:00 committed by GitHub
commit ca873cd6da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 427 additions and 411 deletions

View File

@ -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>

View File

@ -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(),

View File

@ -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);

View File

@ -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'),

View File

@ -44,9 +44,3 @@
box-shadow: 0 2px 5px 0 rgb(0, 0, 0);
}
}
.emojiSelected {
span, i {
color: $color-primary;
}
}

View File

@ -44,6 +44,7 @@ $background-active: darken($color-white, 5%);
flex-direction: column;
justify-content: space-around;
overflow: hidden;
height: 100vh;
}
.header {

View File

@ -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;
}

View File

@ -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.`);
}

View File

@ -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),
];
}

View File

@ -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%;

View File

@ -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,
}
}
/>}

View File

@ -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);

View File

@ -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,
};

View File

@ -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>
);

View File

@ -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();
}

View File

@ -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;

View File

@ -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;

View File

@ -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();
}
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}
/>
);
}
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],
)));
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();
const dropdownTrigger = dropdown.children[0];
const list = findDOMNode(this.list);
const children = [].slice.call(list.children);
children.find(child => child.getAttribute('role') === 'menuitem').focus();
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',
});
this.setState({
isActionsOpen: true,
dropdownVisible: false,
dropdownOffset: dropdownTrigger.offsetTop - scrollContainer.scrollTop,
dropdownDirection: 'top',
});
scrollContainer.addEventListener('scroll', this.handleScroll, false);
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;

View File

@ -9,7 +9,6 @@ export const EMOJI_STATUSES = {
applause: 'applause',
thumbsUp: 'thumbs_up',
thumbsDown: 'thumbs_down',
none: 'clear_status',
};
export default { EMOJI_STATUSES };

View File

@ -200,7 +200,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",