Merge pull request #5839 from KDSBrowne/5782-move-set-status
Move set status menu to user item dropdown
This commit is contained in:
commit
ca873cd6da
@ -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>
|
||||
|
@ -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(),
|
||||
|
@ -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);
|
@ -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'),
|
||||
|
@ -44,9 +44,3 @@
|
||||
box-shadow: 0 2px 5px 0 rgb(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.emojiSelected {
|
||||
span, i {
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ $background-active: darken($color-white, 5%);
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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.`);
|
||||
}
|
||||
|
@ -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),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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%;
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
/>}
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
@ -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;
|
@ -103,3 +103,9 @@
|
||||
max-width: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.emojiSelected {
|
||||
span, i {
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
@ -9,7 +9,6 @@ export const EMOJI_STATUSES = {
|
||||
applause: 'applause',
|
||||
thumbsUp: 'thumbs_up',
|
||||
thumbsDown: 'thumbs_down',
|
||||
none: 'clear_status',
|
||||
};
|
||||
|
||||
export default { EMOJI_STATUSES };
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user