365 lines
11 KiB
JavaScript
Executable file
365 lines
11 KiB
JavaScript
Executable file
/* @flow */
|
|
|
|
import React from 'react';
|
|
import Flux from '../../lib/flux';
|
|
import i18n from '../../i18n';
|
|
import lodash from 'lodash';
|
|
import classNames from 'classnames';
|
|
import fuzzysearch from 'fuzzysearch';
|
|
|
|
import GuildSettingsStore from '../../stores/GuildSettingsStore';
|
|
import GuildMemberStore from '../../stores/GuildMemberStore';
|
|
import UserSettingsStore from '../../stores/UserSettingsStore';
|
|
import UserStore from '../../stores/UserStore';
|
|
import PermissionStore from '../../stores/PermissionStore';
|
|
import StreamerModeStore from '../../stores/StreamerModeStore';
|
|
|
|
import PopoutActionCreators from '../../actions/PopoutActionCreators';
|
|
import GuildSettingsActionCreators from '../../actions/GuildSettingsActionCreators';
|
|
import PruneGuildModalActionCreators from '../../actions/PruneGuildModalActionCreators';
|
|
|
|
import {LazyContentScroller} from '../common/StandardSidebarView';
|
|
import ContextMenu from '../common/ContextMenu';
|
|
import UserContextMenu from '../contextmenus/UserContextMenu';
|
|
import RoleList from '../common/RoleList';
|
|
import SearchableQuickSelect from '../common/SearchableQuickSelect';
|
|
import Tooltip from '../common/Tooltip';
|
|
|
|
import IconButton from '../../uikit/IconButton';
|
|
import OverflowMenuIcon from '../../uikit/icons/OverflowMenuIcon';
|
|
import BotTag from '../../uikit/BotTag';
|
|
import Avatar from '../../uikit/Avatar';
|
|
import Flex from '../../uikit/Flex';
|
|
import {FormTitle, FormTitleTags, FormText, FormTextTypes, FormDivider, FormSection} from '../../uikit/form';
|
|
import SearchBar from '../../uikit/SearchBar';
|
|
|
|
import {ContextMenuTypes, Permissions, ThemeTypes} from '../../Constants';
|
|
|
|
import './GuildSettingsMembers.styl';
|
|
import '../../styles/shared/hover_card.styl';
|
|
|
|
const ROLE_FILTER_POPOUT_ID = 'quick-roles--select';
|
|
|
|
class Member extends React.PureComponent {
|
|
_isMounted = false;
|
|
|
|
state: {
|
|
overflowShown: boolean,
|
|
};
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
overflowShown: false,
|
|
};
|
|
|
|
(this: any).showOverflow = this.showOverflow.bind(this);
|
|
(this: any).handleOverflowClose = this.handleOverflowClose.bind(this);
|
|
}
|
|
|
|
componentDidMount() {
|
|
this._isMounted = true;
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this._isMounted = false;
|
|
}
|
|
|
|
showOverflow(e: Event) {
|
|
const {guild, user} = this.props;
|
|
this.setState({overflowShown: true});
|
|
ContextMenu.openContextMenu(
|
|
e,
|
|
props =>
|
|
<UserContextMenu
|
|
type={ContextMenuTypes.USER_GUILD_MEMBERS}
|
|
user={user}
|
|
guildId={guild.id}
|
|
channelId={guild.id}
|
|
{...props}
|
|
/>,
|
|
{
|
|
onClose: this.handleOverflowClose,
|
|
}
|
|
);
|
|
}
|
|
|
|
renderOwnerHelpIcon() {
|
|
const {user, theme, guild} = this.props;
|
|
|
|
if (user.id !== guild.ownerId) {
|
|
return null;
|
|
}
|
|
|
|
const iconSrc = theme === ThemeTypes.DARK
|
|
? require('../../images/ic_crown_dark_24px.svg')
|
|
: require('../../images/ic_crown_light_24px.svg');
|
|
|
|
return (
|
|
<Tooltip text={i18n.Messages.MEMBER_LIST_SERVER_OWNER_HELP}>
|
|
<IconButton size={IconButton.Sizes.SMALL} src={iconSrc} className="owner-help-icon" />
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const {user, member, streamerMode, guild} = this.props;
|
|
const {overflowShown} = this.state;
|
|
const style = {
|
|
color: member.colorString,
|
|
};
|
|
|
|
return (
|
|
<Flex
|
|
className={classNames('guild-settings-members-member', 'ui-hover-card', {active: overflowShown})}
|
|
align={Flex.Align.CENTER}
|
|
onContextMenu={this.showOverflow}>
|
|
<Avatar className="member-avatar" src={user.getAvatarURL()} />
|
|
<Flex className="name-and-tag" direction={Flex.Direction.VERTICAL} grow={0}>
|
|
<Flex align={Flex.Align.CENTER}>
|
|
<FormText className="name" style={style}>
|
|
{member.nick || user.toString()}
|
|
</FormText>
|
|
{user.bot ? <BotTag /> : null}
|
|
{this.renderOwnerHelpIcon()}
|
|
</Flex>
|
|
{streamerMode ? null : <FormText className="tag">@{user.tag}</FormText>}
|
|
</Flex>
|
|
<Flex.Child className="role-wrapper">
|
|
<div>
|
|
<RoleList user={user} guild={guild} memberRoles={member.roles} />
|
|
</div>
|
|
</Flex.Child>
|
|
<Flex className="overflow-button" align={Flex.Align.CENTER} grow={0} onClick={this.showOverflow}>
|
|
<OverflowMenuIcon />
|
|
</Flex>
|
|
</Flex>
|
|
);
|
|
}
|
|
|
|
handleOverflowClose() {
|
|
if (this._isMounted) {
|
|
this.setState({overflowShown: false});
|
|
}
|
|
}
|
|
}
|
|
|
|
class GuildSettingsMembers extends React.PureComponent {
|
|
state: {
|
|
roleFilterQuery: string,
|
|
};
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
roleFilterQuery: '',
|
|
};
|
|
|
|
(this: any).renderHeader = this.renderHeader.bind(this);
|
|
(this: any).renderPruneAction = this.renderPruneAction.bind(this);
|
|
(this: any).handleQueryChange = this.handleQueryChange.bind(this);
|
|
(this: any).handleQueryClear = this.handleQueryClear.bind(this);
|
|
(this: any).handlePruneClick = this.handlePruneClick.bind(this);
|
|
(this: any).handleRoleFilterQueryChange = this.handleRoleFilterQueryChange.bind(this);
|
|
(this: any).handleRoleFilterQueryClear = this.handleRoleFilterQueryClear.bind(this);
|
|
}
|
|
|
|
componentDidUpdate(prevProps, prevState) {
|
|
if (this.state.roleFilterQuery !== prevState.roleFilterQuery) {
|
|
PopoutActionCreators.rerender(ROLE_FILTER_POPOUT_ID);
|
|
}
|
|
}
|
|
|
|
handlePruneClick() {
|
|
PruneGuildModalActionCreators.open(this.props.guild.id);
|
|
}
|
|
|
|
renderPruneAction() {
|
|
const {canPrune} = this.props;
|
|
|
|
if (!canPrune) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<span>
|
|
{' — '}
|
|
<a onClick={this.handlePruneClick} href="#" className="prune-red-link">
|
|
{i18n.Messages.PRUNE}
|
|
</a>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
renderHeader(filteredMembers) {
|
|
const {roleFilterQuery} = this.state;
|
|
const {guild, searchQuery, selectedRoleId} = this.props;
|
|
|
|
const value = {};
|
|
const items = lodash(guild.roles)
|
|
.sortBy(role => -role.position)
|
|
.filter(role => {
|
|
if (role.id === selectedRoleId) {
|
|
value.label = role.name;
|
|
}
|
|
return fuzzysearch(roleFilterQuery.toLowerCase(), role.name.toLowerCase());
|
|
})
|
|
.map(role => ({
|
|
value: role.id,
|
|
color: role.colorString,
|
|
children: role.name,
|
|
selected: role.id === selectedRoleId,
|
|
}))
|
|
.value();
|
|
|
|
const searchProps = {
|
|
query: roleFilterQuery,
|
|
onChange: this.handleRoleFilterQueryChange,
|
|
onClear: this.handleRoleFilterQueryClear,
|
|
placeholder: i18n.Messages.SEARCH_ROLES,
|
|
};
|
|
|
|
return (
|
|
<FormSection>
|
|
<FormTitle tag={FormTitleTags.H2}>
|
|
{i18n.Messages.GUILD_SETTINGS_MEMBERS_SERVER_MEMBERS}
|
|
</FormTitle>
|
|
<Flex className="guild-settings-members-header" align={Flex.Align.CENTER} style={{height: 20}}>
|
|
<Flex.Child>
|
|
<FormText type={FormTextTypes.DESCRIPTION} className="guild-settings-members-count">
|
|
{i18n.Messages.MEMBERS_HEADER.format({members: filteredMembers.length})}
|
|
{this.renderPruneAction()}
|
|
</FormText>
|
|
</Flex.Child>
|
|
<Flex align={Flex.Align.CENTER} grow={0}>
|
|
<SearchableQuickSelect
|
|
popoutClassName="guild-settings-members-filter-popout elevation-border-low"
|
|
popoutId={ROLE_FILTER_POPOUT_ID}
|
|
className="role-filter"
|
|
items={items}
|
|
label={i18n.Messages.GUILD_SETTINGS_MEMBERS_DISPLAY_ROLE}
|
|
value={value}
|
|
onChange={this.handleRoleChange}
|
|
searchProps={searchProps}
|
|
popoutProps={{
|
|
preventInvert: true,
|
|
position: 'bottom',
|
|
}}
|
|
/>
|
|
<SearchBar query={searchQuery} onChange={this.handleQueryChange} onClear={this.handleQueryClear} />
|
|
</Flex>
|
|
</Flex>
|
|
<FormDivider className="margin-top-20" style={{marginBottom: -1}} />
|
|
</FormSection>
|
|
);
|
|
}
|
|
|
|
makeSearchFilter() {
|
|
let {searchQuery} = this.props;
|
|
if (searchQuery == null || searchQuery.length === 0) {
|
|
return () => true;
|
|
} else {
|
|
return ({user, member}) => {
|
|
searchQuery = searchQuery.toLowerCase();
|
|
const username = user.username.toLowerCase();
|
|
const nick = member.nick != null ? member.nick.toLowerCase() : null;
|
|
return fuzzysearch(searchQuery, username) || (nick != null && fuzzysearch(searchQuery, nick));
|
|
};
|
|
}
|
|
}
|
|
|
|
makeRoleFilter() {
|
|
const {guild, selectedRoleId} = this.props;
|
|
return ({member}) => selectedRoleId === guild.id || member.roles.indexOf(selectedRoleId) !== -1;
|
|
}
|
|
|
|
renderMembers(filteredMembers) {
|
|
const {guild, currentUser, streamerMode, theme} = this.props;
|
|
|
|
return lodash(filteredMembers)
|
|
.sortBy(({comparator}) => comparator)
|
|
.map(({member, user}) =>
|
|
<Member
|
|
currentUser={currentUser}
|
|
member={member}
|
|
user={user}
|
|
guild={guild}
|
|
key={member.userId}
|
|
theme={theme}
|
|
streamerMode={streamerMode}
|
|
/>
|
|
)
|
|
.value();
|
|
}
|
|
|
|
render() {
|
|
const {members} = this.props;
|
|
|
|
const filteredMembers = lodash(members)
|
|
.map(member => {
|
|
const user = UserStore.getUser(member.userId);
|
|
return {
|
|
member,
|
|
user,
|
|
comparator: (user != null ? member.nick || user.username : '').toLowerCase(),
|
|
};
|
|
})
|
|
.filter(this.makeSearchFilter())
|
|
.filter(this.makeRoleFilter())
|
|
.value();
|
|
|
|
return (
|
|
<LazyContentScroller theme={this.props.theme} header={this.renderHeader(filteredMembers)} elementHeight={72}>
|
|
{this.renderMembers(filteredMembers)}
|
|
</LazyContentScroller>
|
|
);
|
|
}
|
|
|
|
handleQueryChange(query) {
|
|
GuildSettingsActionCreators.setSearchQuery(query);
|
|
}
|
|
|
|
handleQueryClear() {
|
|
GuildSettingsActionCreators.setSearchQuery('');
|
|
}
|
|
|
|
handleRoleChange({value}) {
|
|
GuildSettingsActionCreators.selectRole(value);
|
|
}
|
|
|
|
handleRoleFilterQueryChange(roleFilterQuery) {
|
|
this.setState({roleFilterQuery});
|
|
}
|
|
|
|
handleRoleFilterQueryClear() {
|
|
this.setState({
|
|
roleFilterQuery: '',
|
|
});
|
|
}
|
|
}
|
|
|
|
const GuildSettingsMembersWrapped = Flux.connectStores(
|
|
[GuildSettingsStore, UserSettingsStore, GuildMemberStore, StreamerModeStore],
|
|
() => {
|
|
const {guild, searchQuery, selectedRoleId} = GuildSettingsStore.getProps();
|
|
return {
|
|
guild,
|
|
selectedRoleId,
|
|
searchQuery: searchQuery || '',
|
|
members: GuildMemberStore.getMembers(guild.id),
|
|
theme: UserSettingsStore.theme,
|
|
currentUser: UserStore.getCurrentUser(),
|
|
canPrune: PermissionStore.can(Permissions.KICK_MEMBERS, guild),
|
|
streamerMode: StreamerModeStore.enabled,
|
|
};
|
|
}
|
|
)(GuildSettingsMembers);
|
|
|
|
export default GuildSettingsMembersWrapped;
|
|
|
|
|
|
|
|
// WEBPACK FOOTER //
|
|
// ./discord_app/components/guild_settings/GuildSettingsMembers.js
|