diff --git a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx index ea5762df85..19cd42b6ce 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx @@ -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} /> )) } @@ -335,7 +339,7 @@ class UserList extends Component { ); } - + render() { return (
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx index 3667e55d92..5b0e372adb 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx @@ -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} @@ -42,4 +46,6 @@ export default createContainer(({ params }) => ({ openChat: params.chatID, userActions: Service.userActions, isBreakoutRoom: meetingIsBreakout(), + getAvailableActions: Service.getAvailableActions, + normalizeEmojiName: Service.normalizeEmojiName, }), UserListContainer); diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js index 071ccd5546..96ec13fd5b 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/service.js +++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js @@ -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, }; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx index 71a705ff86..c25292b7ff 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/component.jsx @@ -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,297 +61,48 @@ 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 ? : null), + (allowedToMuteAudio ? : null), + (allowedToUnmuteAudio ? : null), + (allowedToResetStatus ? : null), + (allowedToSetPresenter ? : null), + (allowedToKick ? : 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( - - {intl.formatMessage(messages.locked)} - ); - } - - if (user.isGuest) { - userNameSub.push(intl.formatMessage(messages.guest)); - } - - return ( -
- - {user.name} {(user.isCurrent) ? `(${intl.formatMessage(messages.you)})` : ''} - - { - userNameSub.length ? - - {userNameSub.reduce((prev, curr) => [prev, ' | ', curr])} - - : null - } -
- ); - } - - 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 ( -
- { - user.isSharingWebcam ? - - - - : null - } -
- ); - } - - renderUserAction(action, ...parameters) { - const userAction = ( - - ); - - 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 = (); - const userAriaLabel = intl.formatMessage(messages.userAriaLabel, - { - 0: user.name, - 1: presenter, - 2: you, - 3: user.emoji.status, - }); - - const actions = this.getAvailableActions(); - const contents = ( -
- -
- ); - - if (!actions.length) { - return contents; - } - - const { dropdownOffset, dropdownDirection, dropdownVisible } = this.state; - - return ( - { 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" - > - - {contents} - - - - { this.list = ref; }} - getDropdownMenuParent={this.getDropdownMenuParent} - onActionsHide={this.onActionsHide} - > - { - [ - ( - {user.name} - ), - (), - ].concat(actions) - } - - - - ); + return contents; } } diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/user-actions/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/user-actions/component.jsx new file mode 100644 index 0000000000..64943eac9e --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/user-actions/component.jsx @@ -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 = ( + + ); + + return userAction; +}; + +UserActions.propTypes = propTypes; +export default UserActions; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/user-icons/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/user-icons/component.jsx new file mode 100644 index 0000000000..f742d97248 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/user-icons/component.jsx @@ -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 ( +
+ { + user.isSharingWebcam ? + + + + : null + } +
+ ); +}; + +UserIcons.propTypes = propTypes; +export default UserIcons; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/user-list-content/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/user-list-content/component.jsx new file mode 100644 index 0000000000..d291971923 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/user-list-content/component.jsx @@ -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 = ( +
+ +
+ ); + + if (!actions.length) { + return contents; + } + + return ( + { 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" + > + + {contents} + + + + { this.list = ref; }} + getDropdownMenuParent={this.getDropdownMenuParent} + onActionsHide={this.onActionsHide} + > + { + [ + ( + {user.name} + ), + (), + ].concat(actions) + } + + + + ); + } +} + +export default UserListContent; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/user-name/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/user-name/component.jsx new file mode 100644 index 0000000000..c938a50408 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-item/user-name/component.jsx @@ -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( + + {intl.formatMessage(messages.locked)} + ); + } + + if (user.isGuest) { + userNameSub.push(intl.formatMessage(messages.guest)); + } + + return ( +
+ + {user.name} {(user.isCurrent) ? `(${intl.formatMessage(messages.you)})` : ''} + + { + userNameSub.length ? + + {userNameSub.reduce((prev, curr) => [prev, ' | ', curr])} + + : null + } +
+ ); +}; + +UserName.propTypes = propTypes; +export default UserName;