a few refactor made
This commit is contained in:
parent
6efdb1d430
commit
c32a64c02d
@ -255,6 +255,8 @@ class UserList extends Component {
|
||||
intl,
|
||||
makeCall,
|
||||
meeting,
|
||||
getAvailableActions,
|
||||
normalizeEmojiName,
|
||||
} = this.props;
|
||||
|
||||
const userActions = {
|
||||
@ -326,6 +328,8 @@ class UserList extends Component {
|
||||
currentUser={currentUser}
|
||||
userActions={userActions}
|
||||
meeting={meeting}
|
||||
getAvailableActions={getAvailableActions}
|
||||
normalizeEmojiName={normalizeEmojiName}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
@ -16,6 +16,8 @@ const UserListContainer = (props) => {
|
||||
isBreakoutRoom,
|
||||
children,
|
||||
meeting,
|
||||
getAvailableActions,
|
||||
normalizeEmojiName,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@ -28,6 +30,8 @@ const UserListContainer = (props) => {
|
||||
isBreakoutRoom={isBreakoutRoom}
|
||||
makeCall={makeCall}
|
||||
userActions={userActions}
|
||||
getAvailableActions={getAvailableActions}
|
||||
normalizeEmojiName={normalizeEmojiName}
|
||||
>
|
||||
{children}
|
||||
</UserList>
|
||||
@ -42,4 +46,6 @@ export default createContainer(({ params }) => ({
|
||||
openChat: params.chatID,
|
||||
userActions: Service.userActions,
|
||||
isBreakoutRoom: meetingIsBreakout(),
|
||||
getAvailableActions: Service.getAvailableActions,
|
||||
normalizeEmojiName: Service.normalizeEmojiName,
|
||||
}), UserListContainer);
|
||||
|
@ -203,6 +203,28 @@ const getOpenChats = (chatID) => {
|
||||
.sort(sortChats);
|
||||
};
|
||||
|
||||
const getAvailableActions = (currentUser, user, router, isBreakoutRoom) => {
|
||||
const hasAuthority = currentUser.isModerator || user.isCurrent;
|
||||
const allowedToChatPrivately = !user.isCurrent;
|
||||
const allowedToMuteAudio = hasAuthority && user.isVoiceUser && user.isMuted;
|
||||
const allowedToUnmuteAudio = hasAuthority && user.isVoiceUser && !user.isMuted;
|
||||
const allowedToResetStatus = hasAuthority && user.emoji.status !== 'none';
|
||||
|
||||
// if currentUser is a moderator, allow kicking other users
|
||||
const allowedToKick = currentUser.isModerator && !user.isCurrent && !isBreakoutRoom;
|
||||
|
||||
const allowedToSetPresenter = currentUser.isModerator && !user.isPresenter;
|
||||
|
||||
return {
|
||||
allowedToChatPrivately,
|
||||
allowedToMuteAudio,
|
||||
allowedToUnmuteAudio,
|
||||
allowedToResetStatus,
|
||||
allowedToKick,
|
||||
allowedToSetPresenter,
|
||||
};
|
||||
};
|
||||
|
||||
const getCurrentUser = () => {
|
||||
const currentUserId = Auth.userID;
|
||||
const currentUser = Users.findOne({ userId: currentUserId });
|
||||
@ -210,8 +232,24 @@ const getCurrentUser = () => {
|
||||
return (currentUser) ? mapUser(currentUser) : null;
|
||||
};
|
||||
|
||||
const normalizeEmojiName = (emoji) => {
|
||||
const emojisNormalized = {
|
||||
agree: 'thumbs_up',
|
||||
disagree: 'thumbs_down',
|
||||
thumbsUp: 'thumbs_up',
|
||||
thumbsDown: 'thumbs_down',
|
||||
raiseHand: 'hand',
|
||||
away: 'time',
|
||||
neutral: 'undecided',
|
||||
};
|
||||
|
||||
return emoji in emojisNormalized ? emojisNormalized[emoji] : emoji;
|
||||
};
|
||||
|
||||
export default {
|
||||
getUsers,
|
||||
getOpenChats,
|
||||
getCurrentUser,
|
||||
getAvailableActions,
|
||||
normalizeEmojiName,
|
||||
};
|
||||
|
@ -1,19 +1,11 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import UserAvatar from '/imports/ui/components/user-avatar/component';
|
||||
import Icon from '/imports/ui/components/icon/component';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import { withRouter } from 'react-router';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import cx from 'classnames';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import _ from 'lodash';
|
||||
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 UserActions from './user-actions/component';
|
||||
import UserListContent from './user-list-content/component';
|
||||
import styles from './styles.scss';
|
||||
|
||||
const normalizeEmojiName = (emoji) => {
|
||||
@ -42,85 +34,22 @@ const propTypes = {
|
||||
currentUser: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
|
||||
userActions: PropTypes.shape(),
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
shouldShowActions: false,
|
||||
};
|
||||
|
||||
const messages = defineMessages({
|
||||
presenter: {
|
||||
id: 'app.userlist.presenter',
|
||||
description: 'Text for identifying presenter user',
|
||||
},
|
||||
you: {
|
||||
id: 'app.userlist.you',
|
||||
description: 'Text for identifying your user',
|
||||
},
|
||||
locked: {
|
||||
id: 'app.userlist.locked',
|
||||
description: 'Text for identifying locked user',
|
||||
},
|
||||
guest: {
|
||||
id: 'app.userlist.guest',
|
||||
description: 'Text for identifying guest user',
|
||||
},
|
||||
menuTitleContext: {
|
||||
id: 'app.userlist.menuTitleContext',
|
||||
description: 'adds context to userListItem menu title',
|
||||
},
|
||||
userAriaLabel: {
|
||||
id: 'app.userlist.userAriaLabel',
|
||||
description: 'aria label for each user in the userlist',
|
||||
},
|
||||
});
|
||||
|
||||
class UserListItem extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isActionsOpen: false,
|
||||
dropdownOffset: 0,
|
||||
dropdownDirection: 'top',
|
||||
dropdownVisible: false,
|
||||
};
|
||||
|
||||
this.handleScroll = this.handleScroll.bind(this);
|
||||
this.onActionsShow = this.onActionsShow.bind(this);
|
||||
this.onActionsHide = this.onActionsHide.bind(this);
|
||||
this.getDropdownMenuParent = this.getDropdownMenuParent.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.checkDropdownDirection();
|
||||
}
|
||||
|
||||
onActionsShow() {
|
||||
const dropdown = findDOMNode(this.dropdown);
|
||||
const scrollContainer = dropdown.parentElement.parentElement;
|
||||
const dropdownTrigger = dropdown.children[0];
|
||||
|
||||
this.setState({
|
||||
isActionsOpen: true,
|
||||
dropdownVisible: false,
|
||||
dropdownOffset: dropdownTrigger.offsetTop - scrollContainer.scrollTop,
|
||||
dropdownDirection: 'top',
|
||||
});
|
||||
|
||||
scrollContainer.addEventListener('scroll', this.handleScroll, false);
|
||||
}
|
||||
|
||||
getAvailableActions() {
|
||||
getUsersActions() {
|
||||
const {
|
||||
currentUser,
|
||||
user,
|
||||
userActions,
|
||||
router,
|
||||
isBreakoutRoom,
|
||||
getAvailableActions,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -132,298 +61,49 @@ class UserListItem extends Component {
|
||||
unmute,
|
||||
} = userActions;
|
||||
|
||||
const hasAuthority = currentUser.isModerator || user.isCurrent;
|
||||
const allowedToChatPrivately = !user.isCurrent;
|
||||
const allowedToMuteAudio = hasAuthority && user.isVoiceUser && user.isMuted;
|
||||
const allowedToUnmuteAudio = hasAuthority && user.isVoiceUser && !user.isMuted;
|
||||
const allowedToResetStatus = hasAuthority && user.emoji.status !== 'none';
|
||||
const actions = getAvailableActions(currentUser, user, router, isBreakoutRoom);
|
||||
|
||||
// if currentUser is a moderator, allow kicking other users
|
||||
const allowedToKick = currentUser.isModerator && !user.isCurrent && !isBreakoutRoom;
|
||||
|
||||
const allowedToSetPresenter = currentUser.isModerator && !user.isPresenter;
|
||||
const {
|
||||
allowedToChatPrivately,
|
||||
allowedToMuteAudio,
|
||||
allowedToUnmuteAudio,
|
||||
allowedToResetStatus,
|
||||
allowedToKick,
|
||||
allowedToSetPresenter } = actions;
|
||||
|
||||
return _.compact([
|
||||
(allowedToChatPrivately ? this.renderUserAction(openChat, router, user) : null),
|
||||
(allowedToMuteAudio ? this.renderUserAction(unmute, user) : null),
|
||||
(allowedToUnmuteAudio ? this.renderUserAction(mute, user) : null),
|
||||
(allowedToResetStatus ? this.renderUserAction(clearStatus, user) : null),
|
||||
(allowedToSetPresenter ? this.renderUserAction(setPresenter, user) : null),
|
||||
(allowedToKick ? this.renderUserAction(kick, user) : null),
|
||||
(allowedToChatPrivately ? <UserActions action={openChat} options={[router, user]} /> : null),
|
||||
(allowedToMuteAudio ? <UserActions action={unmute} options={[user]} /> : null),
|
||||
(allowedToUnmuteAudio ? <UserActions action={mute} options={[user]} /> : null),
|
||||
(allowedToResetStatus ? <UserActions action={clearStatus} options={[user]} /> : null),
|
||||
(allowedToSetPresenter ? <UserActions action={setPresenter} options={[user]} /> : null),
|
||||
(allowedToKick ? <UserActions action={kick} options={[router, user]} /> : null),
|
||||
]);
|
||||
}
|
||||
|
||||
onActionsHide() {
|
||||
this.setState({
|
||||
isActionsOpen: false,
|
||||
dropdownVisible: false,
|
||||
});
|
||||
|
||||
findDOMNode(this).parentElement.removeEventListener('scroll', this.handleScroll, false);
|
||||
}
|
||||
|
||||
getDropdownMenuParent() {
|
||||
return findDOMNode(this.dropdown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the content fit on the screen, false otherwise.
|
||||
*
|
||||
* @param {number} contentOffSetTop
|
||||
* @param {number} contentOffsetHeight
|
||||
* @return True if the content fit on the screen, false otherwise.
|
||||
*/
|
||||
checkIfDropdownIsVisible(contentOffSetTop, contentOffsetHeight) {
|
||||
return (contentOffSetTop + contentOffsetHeight) < window.innerHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the dropdown is visible, if so, check if should be draw on top or bottom direction.
|
||||
*/
|
||||
checkDropdownDirection() {
|
||||
if (this.isDropdownActivedByUser()) {
|
||||
const dropdown = findDOMNode(this.dropdown);
|
||||
const dropdownTrigger = dropdown.children[0];
|
||||
const dropdownContent = dropdown.children[1];
|
||||
|
||||
const scrollContainer = dropdown.parentElement.parentElement;
|
||||
|
||||
const nextState = {
|
||||
dropdownVisible: true,
|
||||
};
|
||||
|
||||
const isDropdownVisible =
|
||||
this.checkIfDropdownIsVisible(dropdownContent.offsetTop, dropdownContent.offsetHeight);
|
||||
|
||||
if (!isDropdownVisible) {
|
||||
const offsetPageTop =
|
||||
((dropdownTrigger.offsetTop + dropdownTrigger.offsetHeight) - scrollContainer.scrollTop);
|
||||
|
||||
nextState.dropdownOffset = window.innerHeight - offsetPageTop;
|
||||
nextState.dropdownDirection = 'bottom';
|
||||
}
|
||||
|
||||
this.setState(nextState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the dropdown is visible and is opened by the user
|
||||
*
|
||||
* @return True if is visible and opened by the user.
|
||||
*/
|
||||
isDropdownActivedByUser() {
|
||||
const { isActionsOpen, dropdownVisible } = this.state;
|
||||
const list = findDOMNode(this.list);
|
||||
|
||||
if (isActionsOpen && dropdownVisible) {
|
||||
for (let i = 0; i < list.children.length; i++) {
|
||||
if (list.children[i].getAttribute('role') === 'menuitem') {
|
||||
list.children[i].focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isActionsOpen && !dropdownVisible;
|
||||
}
|
||||
|
||||
handleScroll() {
|
||||
this.setState({
|
||||
isActionsOpen: false,
|
||||
});
|
||||
}
|
||||
|
||||
renderUserName() {
|
||||
const {
|
||||
user,
|
||||
intl,
|
||||
compact,
|
||||
} = this.props;
|
||||
|
||||
if (compact) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userNameSub = [];
|
||||
|
||||
if (user.isLocked) {
|
||||
userNameSub.push(<span>
|
||||
<Icon iconName="lock" />
|
||||
{intl.formatMessage(messages.locked)}
|
||||
</span>);
|
||||
}
|
||||
|
||||
if (user.isGuest) {
|
||||
userNameSub.push(intl.formatMessage(messages.guest));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.userName}>
|
||||
<span className={styles.userNameMain}>
|
||||
{user.name} <i>{(user.isCurrent) ? `(${intl.formatMessage(messages.you)})` : ''}</i>
|
||||
</span>
|
||||
{
|
||||
userNameSub.length ?
|
||||
<span className={styles.userNameSub}>
|
||||
{userNameSub.reduce((prev, curr) => [prev, ' | ', curr])}
|
||||
</span>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderUserIcons() {
|
||||
const {
|
||||
user,
|
||||
compact,
|
||||
} = this.props;
|
||||
|
||||
if (compact) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!user.isSharingWebcam) {
|
||||
// Prevent rendering the markup when there is no icon to show
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.userIcons}>
|
||||
{
|
||||
user.isSharingWebcam ?
|
||||
<span className={styles.userIconsContainer}>
|
||||
<Icon iconName="video" />
|
||||
</span>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderUserAction(action, ...parameters) {
|
||||
const userAction = (
|
||||
<DropdownListItem
|
||||
key={_.uniqueId('action-item-')}
|
||||
icon={action.icon}
|
||||
label={action.label}
|
||||
defaultMessage={action.label}
|
||||
onClick={action.handler.bind(this, ...parameters)}
|
||||
placeInTabOrder
|
||||
/>
|
||||
);
|
||||
|
||||
return userAction;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
compact,
|
||||
} = this.props;
|
||||
|
||||
const userItemContentsStyle = {};
|
||||
userItemContentsStyle[styles.userItemContentsCompact] = compact;
|
||||
userItemContentsStyle[styles.active] = this.state.isActionsOpen;
|
||||
|
||||
const {
|
||||
user,
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
const you = (user.isCurrent) ? intl.formatMessage(messages.you) : '';
|
||||
const actions = this.getUsersActions();
|
||||
|
||||
const presenter = (user.isPresenter)
|
||||
? intl.formatMessage(messages.presenter)
|
||||
: '';
|
||||
const contents = (<UserListContent
|
||||
compact={compact}
|
||||
user={user}
|
||||
intl={intl}
|
||||
normalizeEmojiName={normalizeEmojiName}
|
||||
actions={actions}
|
||||
/>);
|
||||
|
||||
const userAriaLabel = intl.formatMessage(messages.userAriaLabel,
|
||||
{
|
||||
0: user.name,
|
||||
1: presenter,
|
||||
2: you,
|
||||
3: user.emoji.status,
|
||||
});
|
||||
|
||||
const actions = this.getAvailableActions();
|
||||
const contents = (
|
||||
<div
|
||||
className={!actions.length ? cx(styles.userListItem, userItemContentsStyle) : null}
|
||||
aria-label={userAriaLabel}
|
||||
>
|
||||
<div className={styles.userItemContents} aria-hidden="true">
|
||||
<div className={styles.userAvatar}>
|
||||
<UserAvatar
|
||||
moderator={user.isModerator}
|
||||
presenter={user.isPresenter}
|
||||
talking={user.isTalking}
|
||||
muted={user.isMuted}
|
||||
listenOnly={user.isListenOnly}
|
||||
voice={user.isVoiceUser}
|
||||
color={user.color}
|
||||
>
|
||||
{user.emoji.status !== 'none' ?
|
||||
<Icon iconName={normalizeEmojiName(user.emoji.status)} /> :
|
||||
user.name.toLowerCase().slice(0, 2)}
|
||||
</UserAvatar>
|
||||
</div>
|
||||
{this.renderUserName()}
|
||||
{this.renderUserIcons()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!actions.length) {
|
||||
return contents;
|
||||
}
|
||||
|
||||
const { dropdownOffset, dropdownDirection, dropdownVisible } = this.state;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
ref={(ref) => { this.dropdown = ref; }}
|
||||
isOpen={this.state.isActionsOpen}
|
||||
onShow={this.onActionsShow}
|
||||
onHide={this.onActionsHide}
|
||||
className={cx(styles.dropdown, styles.userListItem, userItemContentsStyle)}
|
||||
autoFocus={false}
|
||||
aria-haspopup="true"
|
||||
aria-live="assertive"
|
||||
aria-relevant="additions"
|
||||
>
|
||||
<DropdownTrigger>
|
||||
{contents}
|
||||
</DropdownTrigger>
|
||||
<DropdownContent
|
||||
style={{
|
||||
visibility: dropdownVisible ? 'visible' : 'hidden',
|
||||
[dropdownDirection]: `${dropdownOffset}px`,
|
||||
}}
|
||||
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={_.uniqueId('dropdown-list-title')}
|
||||
>
|
||||
{user.name}
|
||||
</DropdownListTitle>),
|
||||
(<DropdownListSeparator key={_.uniqueId('action-separator')} />),
|
||||
].concat(actions)
|
||||
}
|
||||
</DropdownList>
|
||||
</DropdownContent>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UserListItem.propTypes = propTypes;
|
||||
|
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
|
||||
|
||||
const propTypes = {
|
||||
action: PropTypes.shape({
|
||||
icon: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
handler: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
options: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
const UserActions = (props) => {
|
||||
const { action, options } = props;
|
||||
|
||||
const userAction = (
|
||||
<DropdownListItem
|
||||
key={_.uniqueId('action-item-')}
|
||||
icon={action.icon}
|
||||
label={action.label}
|
||||
defaultMessage={action.label}
|
||||
onClick={action.handler.bind(this, ...options)}
|
||||
placeInTabOrder
|
||||
/>
|
||||
);
|
||||
|
||||
return userAction;
|
||||
};
|
||||
|
||||
UserActions.propTypes = propTypes;
|
||||
export default UserActions;
|
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icon from '/imports/ui/components/icon/component';
|
||||
import styles from './../styles.scss';
|
||||
|
||||
const propTypes = {
|
||||
user: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
isPresenter: PropTypes.bool.isRequired,
|
||||
isVoiceUser: PropTypes.bool.isRequired,
|
||||
isModerator: PropTypes.bool.isRequired,
|
||||
image: PropTypes.string,
|
||||
}).isRequired,
|
||||
compact: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const UserIcons = (props) => {
|
||||
const {
|
||||
user,
|
||||
compact,
|
||||
} = props;
|
||||
|
||||
if (compact || user.isSharingWebcam) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.userIcons}>
|
||||
{
|
||||
user.isSharingWebcam ?
|
||||
<span className={styles.userIconsContainer}>
|
||||
<Icon iconName="video" />
|
||||
</span>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
UserIcons.propTypes = propTypes;
|
||||
export default UserIcons;
|
@ -0,0 +1,288 @@
|
||||
import React, { Component } from 'react';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import cx from 'classnames';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import UserAvatar from '/imports/ui/components/user-avatar/component';
|
||||
import Icon from '/imports/ui/components/icon/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 DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component';
|
||||
import DropdownListTitle from '/imports/ui/components/dropdown/list/title/component';
|
||||
import styles from './../styles.scss';
|
||||
import UserName from './../user-name/component';
|
||||
import UserIcons from './../user-icons/component';
|
||||
|
||||
const messages = defineMessages({
|
||||
presenter: {
|
||||
id: 'app.userlist.presenter',
|
||||
description: 'Text for identifying presenter user',
|
||||
},
|
||||
you: {
|
||||
id: 'app.userlist.you',
|
||||
description: 'Text for identifying your user',
|
||||
},
|
||||
locked: {
|
||||
id: 'app.userlist.locked',
|
||||
description: 'Text for identifying locked user',
|
||||
},
|
||||
guest: {
|
||||
id: 'app.userlist.guest',
|
||||
description: 'Text for identifying guest user',
|
||||
},
|
||||
menuTitleContext: {
|
||||
id: 'app.userlist.menuTitleContext',
|
||||
description: 'adds context to userListItem menu title',
|
||||
},
|
||||
userAriaLabel: {
|
||||
id: 'app.userlist.userAriaLabel',
|
||||
description: 'aria label for each user in the userlist',
|
||||
},
|
||||
});
|
||||
|
||||
class UserListContent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isActionsOpen: false,
|
||||
dropdownOffset: 0,
|
||||
dropdownDirection: 'top',
|
||||
dropdownVisible: false,
|
||||
};
|
||||
|
||||
this.handleScroll = this.handleScroll.bind(this);
|
||||
this.onActionsShow = this.onActionsShow.bind(this);
|
||||
this.onActionsHide = this.onActionsHide.bind(this);
|
||||
this.getDropdownMenuParent = this.getDropdownMenuParent.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.checkDropdownDirection();
|
||||
}
|
||||
|
||||
onActionsShow() {
|
||||
const dropdown = findDOMNode(this.dropdown);
|
||||
const scrollContainer = dropdown.parentElement.parentElement;
|
||||
const dropdownTrigger = dropdown.children[0];
|
||||
|
||||
this.setState({
|
||||
isActionsOpen: true,
|
||||
dropdownVisible: false,
|
||||
dropdownOffset: dropdownTrigger.offsetTop - scrollContainer.scrollTop,
|
||||
dropdownDirection: 'top',
|
||||
});
|
||||
|
||||
scrollContainer.addEventListener('scroll', this.handleScroll, false);
|
||||
}
|
||||
|
||||
onActionsHide() {
|
||||
this.setState({
|
||||
isActionsOpen: false,
|
||||
dropdownVisible: false,
|
||||
});
|
||||
|
||||
findDOMNode(this).parentElement.removeEventListener('scroll', this.handleScroll, false);
|
||||
}
|
||||
|
||||
getDropdownMenuParent() {
|
||||
return findDOMNode(this.dropdown);
|
||||
}
|
||||
|
||||
handleScroll() {
|
||||
this.setState({
|
||||
isActionsOpen: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the dropdown is visible, if so, check if should be draw on top or bottom direction.
|
||||
*/
|
||||
checkDropdownDirection() {
|
||||
if (this.isDropdownActivedByUser()) {
|
||||
const dropdown = findDOMNode(this.dropdown);
|
||||
const dropdownTrigger = dropdown.children[0];
|
||||
const dropdownContent = dropdown.children[1];
|
||||
|
||||
const scrollContainer = dropdown.parentElement.parentElement;
|
||||
|
||||
const nextState = {
|
||||
dropdownVisible: true,
|
||||
};
|
||||
|
||||
const isDropdownVisible =
|
||||
this.checkIfDropdownIsVisible(dropdownContent.offsetTop, dropdownContent.offsetHeight);
|
||||
|
||||
if (!isDropdownVisible) {
|
||||
const offsetPageTop =
|
||||
((dropdownTrigger.offsetTop + dropdownTrigger.offsetHeight) - scrollContainer.scrollTop);
|
||||
|
||||
nextState.dropdownOffset = window.innerHeight - offsetPageTop;
|
||||
nextState.dropdownDirection = 'bottom';
|
||||
}
|
||||
|
||||
this.setState(nextState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the content fit on the screen, false otherwise.
|
||||
*
|
||||
* @param {number} contentOffSetTop
|
||||
* @param {number} contentOffsetHeight
|
||||
* @return True if the content fit on the screen, false otherwise.
|
||||
*/
|
||||
checkIfDropdownIsVisible(contentOffSetTop, contentOffsetHeight) {
|
||||
return (contentOffSetTop + contentOffsetHeight) < window.innerHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the dropdown is visible and is opened by the user
|
||||
*
|
||||
* @return True if is visible and opened by the user.
|
||||
*/
|
||||
isDropdownActivedByUser() {
|
||||
const { isActionsOpen, dropdownVisible } = this.state;
|
||||
if (isActionsOpen && dropdownVisible) {
|
||||
this.focusDropdown();
|
||||
}
|
||||
return isActionsOpen && !dropdownVisible;
|
||||
}
|
||||
|
||||
focusDropdown() {
|
||||
const list = findDOMNode(this.list);
|
||||
for (let i = 0; i < list.children.length; i++) {
|
||||
if (list.children[i].getAttribute('role') === 'menuitem') {
|
||||
list.children[i].focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The list children is a instance of HTMLCollection, there is no find, some, etc methods
|
||||
/* const childrens = [].slice.call(list.children);
|
||||
|
||||
childrens.find(child => child.getAttribute('role') === 'menuitem').focus(); */
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
compact,
|
||||
user,
|
||||
intl,
|
||||
normalizeEmojiName,
|
||||
actions,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isActionsOpen,
|
||||
dropdownVisible,
|
||||
dropdownDirection,
|
||||
dropdownOffset,
|
||||
} = this.state;
|
||||
const userItemContentsStyle = {};
|
||||
|
||||
userItemContentsStyle[styles.userItemContentsCompact] = compact;
|
||||
userItemContentsStyle[styles.active] = isActionsOpen;
|
||||
|
||||
const you = (user.isCurrent) ? intl.formatMessage(messages.you) : '';
|
||||
|
||||
const presenter = (user.isPresenter)
|
||||
? intl.formatMessage(messages.presenter)
|
||||
: '';
|
||||
|
||||
const userAriaLabel = intl.formatMessage(messages.userAriaLabel,
|
||||
{
|
||||
0: user.name,
|
||||
1: presenter,
|
||||
2: you,
|
||||
3: user.emoji.status,
|
||||
});
|
||||
|
||||
const contents = (
|
||||
<div
|
||||
className={!actions.length ? cx(styles.userListItem, userItemContentsStyle) : null}
|
||||
aria-label={userAriaLabel}
|
||||
>
|
||||
<div className={styles.userItemContents} aria-hidden="true">
|
||||
<div className={styles.userAvatar}>
|
||||
<UserAvatar
|
||||
moderator={user.isModerator}
|
||||
presenter={user.isPresenter}
|
||||
talking={user.isTalking}
|
||||
muted={user.isMuted}
|
||||
listenOnly={user.isListenOnly}
|
||||
voice={user.isVoiceUser}
|
||||
color={user.color}
|
||||
>
|
||||
{user.emoji.status !== 'none' ?
|
||||
<Icon iconName={normalizeEmojiName(user.emoji.status)} /> :
|
||||
user.name.toLowerCase().slice(0, 2)}
|
||||
</UserAvatar>
|
||||
</div>
|
||||
{<UserName
|
||||
user={user}
|
||||
compact={compact}
|
||||
intl={intl}
|
||||
/>}
|
||||
{<UserIcons
|
||||
user={user}
|
||||
compact={compact}
|
||||
/>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!actions.length) {
|
||||
return contents;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
ref={(ref) => { this.dropdown = ref; }}
|
||||
isOpen={this.state.isActionsOpen}
|
||||
onShow={this.onActionsShow}
|
||||
onHide={this.onActionsHide}
|
||||
className={cx(styles.dropdown, styles.userListItem, userItemContentsStyle)}
|
||||
autoFocus={false}
|
||||
aria-haspopup="true"
|
||||
aria-live="assertive"
|
||||
aria-relevant="additions"
|
||||
>
|
||||
<DropdownTrigger>
|
||||
{contents}
|
||||
</DropdownTrigger>
|
||||
<DropdownContent
|
||||
style={{
|
||||
visibility: dropdownVisible ? 'visible' : 'hidden',
|
||||
[dropdownDirection]: `${dropdownOffset}px`,
|
||||
}}
|
||||
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={_.uniqueId('dropdown-list-title')}
|
||||
>
|
||||
{user.name}
|
||||
</DropdownListTitle>),
|
||||
(<DropdownListSeparator key={_.uniqueId('action-separator')} />),
|
||||
].concat(actions)
|
||||
}
|
||||
</DropdownList>
|
||||
</DropdownContent>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UserListContent;
|
@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import Icon from '/imports/ui/components/icon/component';
|
||||
import styles from './../styles.scss';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
presenter: {
|
||||
id: 'app.userlist.presenter',
|
||||
description: 'Text for identifying presenter user',
|
||||
},
|
||||
you: {
|
||||
id: 'app.userlist.you',
|
||||
description: 'Text for identifying your user',
|
||||
},
|
||||
locked: {
|
||||
id: 'app.userlist.locked',
|
||||
description: 'Text for identifying locked user',
|
||||
},
|
||||
guest: {
|
||||
id: 'app.userlist.guest',
|
||||
description: 'Text for identifying guest user',
|
||||
},
|
||||
menuTitleContext: {
|
||||
id: 'app.userlist.menuTitleContext',
|
||||
description: 'adds context to userListItem menu title',
|
||||
},
|
||||
userAriaLabel: {
|
||||
id: 'app.userlist.userAriaLabel',
|
||||
description: 'aria label for each user in the userlist',
|
||||
},
|
||||
});
|
||||
const propTypes = {
|
||||
user: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
isPresenter: PropTypes.bool.isRequired,
|
||||
isVoiceUser: PropTypes.bool.isRequired,
|
||||
isModerator: PropTypes.bool.isRequired,
|
||||
image: PropTypes.string,
|
||||
}).isRequired,
|
||||
compact: PropTypes.bool.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const UserName = (props) => {
|
||||
const {
|
||||
user,
|
||||
intl,
|
||||
compact,
|
||||
} = props;
|
||||
|
||||
if (compact) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userNameSub = [];
|
||||
|
||||
if (user.isLocked) {
|
||||
userNameSub.push(<span>
|
||||
<Icon iconName="lock" />
|
||||
{intl.formatMessage(messages.locked)}
|
||||
</span>);
|
||||
}
|
||||
|
||||
if (user.isGuest) {
|
||||
userNameSub.push(intl.formatMessage(messages.guest));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.userName}>
|
||||
<span className={styles.userNameMain}>
|
||||
{user.name} <i>{(user.isCurrent) ? `(${intl.formatMessage(messages.you)})` : ''}</i>
|
||||
</span>
|
||||
{
|
||||
userNameSub.length ?
|
||||
<span className={styles.userNameSub}>
|
||||
{userNameSub.reduce((prev, curr) => [prev, ' | ', curr])}
|
||||
</span>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
UserName.propTypes = propTypes;
|
||||
export default UserName;
|
Loading…
Reference in New Issue
Block a user