2017-06-08_509bba0/509bba0_unpacked_with_node_.../discord_app/components/guild_settings/GuildSettingsAuditLog.js

945 lines
31 KiB
JavaScript
Executable File

/* @flow */
import React from 'react';
import ReactDOM from 'react-dom';
import lodash from 'lodash';
import fuzzysearch from 'fuzzysearch';
import Flux from '../../lib/flux';
import GuildSettingsAuditLogStore from '../../stores/GuildSettingsAuditLogStore';
import {fetchLogs, fetchNextLogPage, filterByAction, filterByUserId} from '../../actions/AuditLogActionCreators';
import PopoutActionCreators from '../../actions/PopoutActionCreators';
import AuditLogRecord, {AuditLogChange, getActionType, getTargetType} from '../../records/AuditLogRecord';
import AuditLog, {AuditLogIcon} from '../../uikit/AuditLog';
import Avatar from '../../uikit/Avatar';
import MembersIcon from '../../uikit/icons/MembersIcon';
import EmptyState, {EmptyStateText, EmptyStateImage} from '../../uikit/EmptyState';
import Spinner from '../../uikit/Spinner';
import Flex from '../../uikit/Flex';
import Button from '../../uikit/Button';
import Scroller from '../../uikit/Scroller';
import {FormTitle, FormTitleTags, FormDivider} from '../../uikit/form';
import SelectableItem from '../../uikit/SelectableItem';
import SearchableQuickSelect from '../common/SearchableQuickSelect';
import ContextMenu from '../common/ContextMenu';
import StreamerModeEnabled from '../StreamerModeEnabled';
import UserContextMenu from '../contextmenus/UserContextMenu';
import ChannelContextMenu from '../contextmenus/ChannelContextMenu';
import DeveloperContextMenu from '../contextmenus/DeveloperContextMenu';
import GuildStore from '../../stores/GuildStore';
import ChannelStore from '../../stores/ChannelStore';
import StreamerModeStore from '../../stores/StreamerModeStore';
import UserStore from '../../stores/UserStore';
import EmojiStore from '../../stores/EmojiStore';
import UserSettingsStore from '../../stores/UserSettingsStore';
import GuildSettingsStore from '../../stores/GuildSettingsStore';
import InstantInviteUtils from '../../utils/InstantInviteUtils';
import {int2hex} from '../../../discord_common/js/utils/ColorUtils';
import UserRecord from '../../records/UserRecord';
import {
AuditLogActions,
AuditLogTargetTypes,
AuditLogChangeKeys,
AuditLogSubtargetTypes,
Permissions,
NOOP_NULL,
ThemeTypes,
Colors,
ContextMenuTypes,
} from '../../Constants';
import i18n from '../../i18n';
import type GuildRecord from '../../records/GuildRecord';
import type ChannelRecord from '../../records/ChannelRecord';
import './GuildSettingsAuditLog.styl';
type RectData = {
lastExpanded: ?ClientRect,
expanded: ?ClientRect,
};
const MAX_GROUP_FETCH_NEXT_PAGES = 2;
const USER_FILTER_POPOUT_ID = 'guild-settings-audit-logs-user-filter';
const ACTION_FILTER_POPOUT_ID = 'guild-settings-audit-logs-action-filter';
function getPermissionChanges(oldPermissions?, newPermissions?) {
const oldPerms = typeof oldPermissions === 'number' ? oldPermissions : 0;
const newPerms = typeof newPermissions === 'number' ? newPermissions : 0;
const addedPermissionBits = newPerms & ~oldPerms;
const removedPermissionBits = oldPerms & ~newPerms;
const added = [];
const removed = [];
for (const key in Permissions) {
const permission = +Permissions[key];
if ((addedPermissionBits & permission) === permission) {
added.push(permission);
}
if ((removedPermissionBits & permission) === permission) {
removed.push(permission);
}
}
return {added, removed};
}
function convertValue<T>(
change: AuditLogChange,
convertFunction: (value: any) => ?T,
toString?: (value: T) => string
): AuditLogChange {
let newValue = change.newValue;
let oldValue = change.oldValue;
if (change.newValue != null) {
newValue = convertFunction(change.newValue);
if (toString != null && newValue != null) {
newValue = toString(newValue);
}
}
if (change.oldValue != null) {
oldValue = convertFunction(change.oldValue);
if (toString != null && oldValue != null) {
oldValue = toString(oldValue);
}
}
return new AuditLogChange(change.key, oldValue, newValue);
}
function getTargetValue<T>(
log: AuditLogRecord,
keyForValue: string,
getTarget: (id: string) => ?T,
toString: (obj: T) => string | UserRecord | ChannelRecord,
targetId?: string
) {
let target = null;
targetId = targetId || log.targetId;
// Attempt to grab from a store or however the object is stored
const targetFromParam = getTarget(targetId);
if (targetFromParam != null) {
target = toString(targetFromParam);
}
// Check deleted targets
if (target == null) {
const deletedTargets = GuildSettingsAuditLogStore.deletedTargets[log.targetType];
if (deletedTargets != null && deletedTargets[targetId]) {
target = deletedTargets[targetId];
}
}
// If it is any other type of change, check the new and old values
if (target == null && log.changes != null) {
const change = log.changes.find(change => change.key === keyForValue);
if (change != null) {
target = change.newValue || change.oldValue;
}
}
return target || log.targetId;
}
function convertSubtarget<T>(id: string, getSubtarget: (id: string) => ?T, toString?: (obj: T) => string) {
let subtarget = id;
const subtargetFromParam = getSubtarget(id);
if (subtargetFromParam != null && toString != null) {
subtarget = toString(subtargetFromParam);
}
return subtarget;
}
function transformTarget(log: AuditLogRecord, guild: GuildRecord) {
switch (log.targetType) {
case AuditLogTargetTypes.GUILD:
return guild;
case AuditLogTargetTypes.CHANNEL:
return getTargetValue(
log,
AuditLogChangeKeys.NAME,
id => ChannelStore.getChannel(id),
channel => channel.toString(true)
);
case AuditLogTargetTypes.USER:
return getTargetValue(log, AuditLogChangeKeys.NICK, id => UserStore.getUser(id), user => user);
case AuditLogTargetTypes.ROLE:
return getTargetValue(log, AuditLogChangeKeys.NAME, id => guild.getRole(id), role => role.name);
case AuditLogTargetTypes.INVITE:
return getTargetValue(log, AuditLogChangeKeys.CODE, NOOP_NULL, invite => invite.code);
case AuditLogTargetTypes.WEBHOOK:
return getTargetValue(
log,
AuditLogChangeKeys.NAME,
id => GuildSettingsAuditLogStore.webhooks.find(webhook => webhook.id === id),
webhook => webhook.name
);
case AuditLogTargetTypes.EMOJI:
return getTargetValue(
log,
AuditLogChangeKeys.NAME,
id => EmojiStore.getGuildEmoji(guild.id).find(emoji => emoji.id === id),
emoji => emoji.name
);
default:
console.warn('[GuildSettingsAuditLog] Unknown targetType for log', log);
return null;
}
}
function transformChange(change: AuditLogChange) {
switch (change.key) {
case AuditLogChangeKeys.OWNER_ID:
return convertValue(change, v => UserStore.getUser(v));
case AuditLogChangeKeys.AFK_CHANNEL_ID:
return convertValue(change, v => ChannelStore.getChannel(v), channel => channel.toString(true));
case AuditLogChangeKeys.AFK_TIMEOUT:
return convertValue(change, v => v / 60);
case AuditLogChangeKeys.BITRATE:
return convertValue(change, v => v / 1000);
case AuditLogChangeKeys.COLOR:
return convertValue(change, v => int2hex(v).toUpperCase());
case AuditLogChangeKeys.MAX_AGE: {
return convertValue(change, v => {
const option = InstantInviteUtils.getMaxAgeOptions.find(({value}) => `${v}` === value);
if (option) {
return option.label;
} else {
return v;
}
});
}
case AuditLogChangeKeys.CHANNEL_ID: {
return convertValue(change, v => ChannelStore.getChannel(v), channel => channel.toString(true));
}
case AuditLogChangeKeys.PERMISSIONS: {
const newChanges = [];
const {added, removed} = getPermissionChanges(change.oldValue, change.newValue);
if (added.length > 0) {
const addChange = new AuditLogChange(AuditLogChangeKeys.PERMISSIONS_GRANTED, null, added);
newChanges.push(addChange);
}
if (removed.length) {
const removeChange = new AuditLogChange(AuditLogChangeKeys.PERMISSIONS_DENIED, null, removed);
newChanges.push(removeChange);
}
return newChanges;
}
case AuditLogChangeKeys.PERMISSIONS_GRANTED:
case AuditLogChangeKeys.PERMISSIONS_DENIED: {
const newChanges = [];
const {added} = getPermissionChanges(change.oldValue, change.newValue);
if (added.length > 0) {
const addChange = new AuditLogChange(change.key, null, added);
newChanges.push(addChange);
}
return newChanges;
}
}
return change;
}
function transformOptions(log: AuditLogRecord) {
if (log.options != null) {
const newOptions = {...log.options};
switch (log.options.type) {
case AuditLogSubtargetTypes.USER: {
newOptions.subtarget = convertSubtarget(log.options.id, id => UserStore.getUser(id), user => user.toString());
break;
}
case AuditLogSubtargetTypes.ROLE: {
newOptions.subtarget = convertSubtarget(log.options.role_name, NOOP_NULL);
break;
}
}
if (log.options.channel_id != null) {
newOptions.channel = getTargetValue(
log,
'',
id => ChannelStore.getChannel(id),
channel => channel,
log.options.channel_id
);
}
if (log.options.members_removed) {
newOptions.count = log.options.members_removed;
}
return newOptions;
}
return log.options;
}
function transformLogs(logs: Array<AuditLogRecord>, guild: GuildRecord): Array<AuditLogRecord> {
const newLogs = [];
logs.forEach(log => {
const newTarget = transformTarget(log, guild);
const newUser = UserStore.getUser(log.userId);
if ((newTarget == null && log.action !== AuditLogActions.MEMBER_PRUNE) || newUser == null) {
return;
}
log = log.set('user', newUser);
log = log.set('target', newTarget);
log = log.set('options', transformOptions(log));
if (log.changes != null) {
const newChanges = [];
log.changes.forEach((change: AuditLogChange) => {
const newChange = transformChange(change);
if (Array.isArray(newChange)) {
newChange.forEach(c => newChanges.push(c));
} else {
newChanges.push(newChange);
}
});
log = log.set('changes', newChanges);
}
newLogs.push(log);
});
return newLogs;
}
const ACTION_FILTER_ITEMS = () => [
{
value: AuditLogActions.ALL,
label: i18n.Messages.GUILD_SETTINGS_FILTER_ALL_ACTIONS,
valueLabel: i18n.Messages.GUILD_SETTINGS_FILTER_ALL,
},
{value: AuditLogActions.GUILD_UPDATE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_GUILD_UPDATE},
{value: AuditLogActions.CHANNEL_CREATE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_CHANNEL_CREATE},
{value: AuditLogActions.CHANNEL_UPDATE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_CHANNEL_UPDATE},
{value: AuditLogActions.CHANNEL_DELETE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_CHANNEL_DELETE},
{
value: AuditLogActions.CHANNEL_OVERWRITE_CREATE,
label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_CHANNEL_OVERWRITE_CREATE,
},
{
value: AuditLogActions.CHANNEL_OVERWRITE_UPDATE,
label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_CHANNEL_OVERWRITE_UPDATE,
},
{
value: AuditLogActions.CHANNEL_OVERWRITE_DELETE,
label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_CHANNEL_OVERWRITE_DELETE,
},
{value: AuditLogActions.MEMBER_KICK, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_MEMBER_KICK},
{value: AuditLogActions.MEMBER_PRUNE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_MEMBER_PRUNE},
{value: AuditLogActions.MEMBER_BAN_ADD, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_MEMBER_BAN_ADD},
{value: AuditLogActions.MEMBER_BAN_REMOVE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_MEMBER_BAN_REMOVE},
{value: AuditLogActions.MEMBER_UPDATE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_MEMBER_UPDATE},
{value: AuditLogActions.MEMBER_ROLE_UPDATE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_MEMBER_ROLE_UPDATE},
{value: AuditLogActions.ROLE_CREATE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_ROLE_CREATE},
{value: AuditLogActions.ROLE_UPDATE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_ROLE_UPDATE},
{value: AuditLogActions.ROLE_DELETE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_ROLE_DELETE},
{value: AuditLogActions.INVITE_CREATE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_INVITE_CREATE},
{value: AuditLogActions.INVITE_UPDATE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_INVITE_UPDATE},
{value: AuditLogActions.INVITE_DELETE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_INVITE_DELETE},
{value: AuditLogActions.WEBHOOK_CREATE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_WEBHOOK_CREATE},
{value: AuditLogActions.WEBHOOK_UPDATE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_WEBHOOK_UPDATE},
{value: AuditLogActions.WEBHOOK_DELETE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_WEBHOOK_DELETE},
{value: AuditLogActions.EMOJI_CREATE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_EMOJI_CREATE},
{value: AuditLogActions.EMOJI_UPDATE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_EMOJI_UPDATE},
{value: AuditLogActions.EMOJI_DELETE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_EMOJI_DELETE},
{value: AuditLogActions.MESSAGE_DELETE, label: i18n.Messages.GUILD_SETTINGS_ACTION_FILTER_MESSAGE_DELETE},
];
class AuditLogClickWrap extends React.PureComponent {
constructor(props: any) {
super(props);
(this: any).handleHeaderClick = this.handleHeaderClick.bind(this);
(this: any).handleUserContextMenu = this.handleUserContextMenu.bind(this);
(this: any).handleChannelContextMenu = this.handleChannelContextMenu.bind(this);
(this: any).handleTargetContextMenu = this.handleTargetContextMenu.bind(this);
}
render() {
// eslint-disable-next-line no-unused-vars
const {onHeaderClick, guildId, ...props} = this.props;
return (
<AuditLog
{...props}
onHeaderClick={this.handleHeaderClick}
onUserContextMenu={this.handleUserContextMenu}
onChannelContextMenu={this.handleChannelContextMenu}
onTargetContextMenu={this.handleTargetContextMenu}
/>
);
}
handleHeaderClick(e: Event) {
const {onHeaderClick, log} = this.props;
onHeaderClick && onHeaderClick(log, e);
}
handleUserContextMenu(e: Event) {
const {log, guildId} = this.props;
ContextMenu.openContextMenu(e, props =>
<UserContextMenu {...props} type={ContextMenuTypes.USER_AUDIT_LOG} guildId={guildId} user={log.user} />
);
}
handleChannelContextMenu(e: Event) {
const {log, guildId} = this.props;
const guild = GuildStore.getGuild(guildId);
if (log.options.channel != null && guild != null) {
ContextMenu.openContextMenu(e, props =>
<ChannelContextMenu
{...props}
type={ContextMenuTypes.CHANNEL_AUDIT_LOG}
guild={guild}
channel={log.options.channel}
/>
);
}
}
handleTargetContextMenu(e: Event) {
const {log, guildId} = this.props;
ContextMenu.openContextMenu(e, props => {
switch (log.targetType) {
case AuditLogTargetTypes.CHANNEL:
const channel = ChannelStore.getChannel(log.targetId);
const guild = GuildStore.getGuild(guildId);
if (channel != null && guild != null) {
return (
<ChannelContextMenu
{...props}
type={ContextMenuTypes.CHANNEL_AUDIT_LOG}
guild={guild}
channel={channel}
/>
);
} else {
return <DeveloperContextMenu {...props} id={log.targetId} />;
}
case AuditLogTargetTypes.USER:
const user = UserStore.getUser(log.targetId);
if (user != null) {
return <UserContextMenu {...props} type={ContextMenuTypes.USER_AUDIT_LOG} guildId={guildId} user={user} />;
}
break;
}
return null;
});
}
}
class GuildSettingsAuditLog extends React.PureComponent {
_clickedInside = false;
_scrollerRef: Scroller;
_expandedRef: ?AuditLogClickWrap;
_lastExpandedRef: ?AuditLogClickWrap;
_prevRects: RectData;
state: {
expandedId: ?string,
lastExpandedId: ?string,
userFilterQuery: string,
actionFilterQuery: string,
};
constructor(props: any) {
super(props);
this.state = {
expandedId: null,
lastExpandedId: null,
userFilterQuery: '',
actionFilterQuery: '',
};
lodash.bindAll(this, [
'renderActionQuickSelectItem',
'renderUserQuickSelectItem',
'renderHeaderDropdowns',
'handleFilterActionChange',
'handleFilterUserChange',
'handleHeaderClick',
'handleOutsideClick',
'handleContentClick',
'handleOnScroll',
'handleSetScrollerRef',
'handleUserFilterQueryChange',
'handleUserFilterQueryClear',
'handleActionFilterQueryChange',
'handleActionFilterQueryClear',
'handleSetExpandedRef',
'handleSetLastExpandedRef',
'handleFetchNextPage',
]);
}
componentDidMount() {
fetchLogs(this.props.guildId);
document.addEventListener('click', this.handleOutsideClick);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleOutsideClick);
}
componentWillUpdate(_, nextState) {
if (this.state.expandedId !== nextState.expandedId) {
this._prevRects = this.getRects();
}
}
componentDidUpdate(prevProps, prevState) {
if (this.state.expandedId !== prevState.expandedId) {
this.fixScroll();
}
if (
!this.props.showLoadMore &&
this.props.logs.length !== prevProps.logs.length &&
this.isScrollerAtBottom(this._scrollerRef)
) {
fetchNextLogPage(this.props.guildId, true);
}
}
isScrollerAtBottom(scroller: Scroller) {
const {offsetHeight, scrollHeight, scrollTop} = scroller.getScrollData();
return scrollTop + offsetHeight >= scrollHeight;
}
fixScroll() {
const rects = this.getRects();
const prevRects = this._prevRects;
// If we are not transitioning from one open to another, the scroll doesn't have to change
if (rects.expanded == null || rects.lastExpanded == null || prevRects.expanded == null) {
return;
}
// If the opening log is above, the scroll doesn't have to change
if (rects.expanded.top < rects.lastExpanded.top) {
return;
}
const heightDiff = prevRects.expanded.height - rects.lastExpanded.height;
const scrollData = this._scrollerRef.getScrollData();
const scrollTo = scrollData.scrollTop - heightDiff;
this._scrollerRef.scrollTo(scrollTo);
}
getRects() {
const lastExpandedNode = ReactDOM.findDOMNode(this._lastExpandedRef);
const expandedNode = ReactDOM.findDOMNode(this._expandedRef);
const rects: RectData = {
lastExpanded: null,
expanded: null,
};
if (lastExpandedNode != null && lastExpandedNode instanceof Element) {
rects.lastExpanded = lastExpandedNode.getBoundingClientRect();
}
if (expandedNode != null && expandedNode instanceof Element) {
rects.expanded = expandedNode.getBoundingClientRect();
}
return rects;
}
renderActionQuickSelectItem(item: *, index: number) {
const actionType = getActionType(item.value);
const targetType = getTargetType(item.value);
return (
<SelectableItem
className="action-item"
selected={item.selected}
key={index}
onClick={() => this.handleFilterActionChange(item)}>
<AuditLogIcon
themeOverride={item.selected ? ThemeTypes.DARK : null}
actionType={actionType}
targetType={targetType}
action={item.value}
/>
<Flex.Child>{item.label}</Flex.Child>
</SelectableItem>
);
}
renderUserQuickSelectItem(item: *, index: number) {
let content = null;
if (item.label instanceof UserRecord) {
const user = item.label;
content = [
<Flex.Child key="avatar"><Avatar size={Avatar.Sizes.SMALL} src={user.getAvatarURL()} /></Flex.Child>,
<Flex.Child key="user-text" className="user-text">
<span className="username">{user.username}</span>
<span className="discriminator">#{user.discriminator}</span>
</Flex.Child>,
];
} else {
content = [
<Flex.Child key="avatar" grow={0} shrink={0}>
<MembersIcon />
</Flex.Child>,
<Flex.Child key="user-text">
{item.label}
</Flex.Child>,
];
}
return (
<SelectableItem
selected={item.selected}
onClick={() => this.handleFilterUserChange(item)}
key={index}
style={{height: 'auto'}}>
{content}
</SelectableItem>
);
}
renderUserQuickSelectValue(value) {
if (value instanceof UserRecord) {
return value.username;
} else {
return value.valueLabel || value.label;
}
}
renderActionQuickSelectValue(value) {
return value.valueLabel || value.label;
}
renderHeaderDropdowns() {
const {actionFilter, hide, userIdFilter, moderators} = this.props;
const {userFilterQuery, actionFilterQuery} = this.state;
if (hide) {
return null;
}
const allActionItems = ACTION_FILTER_ITEMS().map(action => ({...action, selected: action.value === actionFilter}));
const actionItems = allActionItems.filter(action =>
fuzzysearch(actionFilterQuery.toLowerCase(), action.label.toLowerCase())
);
const actionValue = allActionItems.find(({value}) => actionFilter === value);
const searchActionProps = {
query: actionFilterQuery,
onChange: this.handleActionFilterQueryChange,
onClear: this.handleActionFilterQueryClear,
placeholder: i18n.Messages.SEARCH_ACTIONS,
};
const allUserIdOption = {
label: i18n.Messages.GUILD_SETTINGS_FILTER_ALL_USERS,
valueLabel: i18n.Messages.GUILD_SETTINGS_FILTER_ALL,
value: null,
selected: userIdFilter == null,
};
const allUserItems = [allUserIdOption, ...moderators];
const userItems = allUserItems
.filter(user => {
const query = userFilterQuery.toLowerCase();
if (user instanceof UserRecord) {
return fuzzysearch(query, user.username.toLowerCase());
} else {
return fuzzysearch(query, user.label);
}
})
.map(user => {
if (user instanceof UserRecord) {
return {label: user, value: user.id, selected: user.id === userIdFilter};
} else {
return user;
}
});
const userValue =
allUserItems.find(user => {
if (user instanceof UserRecord) {
return user.id === userIdFilter;
}
return user.value === userIdFilter;
}) || allUserIdOption;
const searchUsersProps = {
query: userFilterQuery,
onChange: this.handleUserFilterQueryChange,
onClear: this.handleUserFilterQueryClear,
placeholder: i18n.Messages.SEARCH_MEMBERS,
};
return [
<Flex.Child key="user-filter">
<SearchableQuickSelect
popoutId={USER_FILTER_POPOUT_ID}
popoutClassName="guild-settings-audit-logs-user-filter-popout elevation-border-high"
items={userItems}
renderItem={this.renderUserQuickSelectItem}
renderValue={this.renderUserQuickSelectValue}
value={userValue}
onChange={this.handleFilterUserChange}
label={i18n.Messages.GUILD_SETTINGS_FILTER_USER}
searchProps={searchUsersProps}
popoutProps={{
preventInvert: true,
position: 'bottom',
}}
/>
</Flex.Child>,
<Flex.Child key="action-filter">
<SearchableQuickSelect
popoutId={ACTION_FILTER_POPOUT_ID}
popoutClassName="guild-settings-audit-logs-action-filter-popout elevation-border-low"
items={actionItems}
renderItem={this.renderActionQuickSelectItem}
renderValue={this.renderActionQuickSelectValue}
value={actionValue}
onChange={this.handleFilterActionChange}
label={i18n.Messages.GUILD_SETTINGS_FILTER_ACTION}
searchProps={searchActionProps}
popoutProps={{
preventInvert: true,
position: 'bottom',
}}
/>
</Flex.Child>,
];
}
renderHeader() {
return (
<Flex direction={Flex.Direction.VERTICAL} className="custom-header">
<Flex align={Flex.Align.CENTER}>
<FormTitle tag={FormTitleTags.H2} className="margin-reset">
{i18n.Messages.GUILD_SETTINGS_LABEL_AUDIT_LOG}
</FormTitle>
{this.renderHeaderDropdowns()}
</Flex>
<FormDivider className="margin-top-20 margin-bottom-20" />
</Flex>
);
}
renderSpinner() {
return <Spinner type={Spinner.Type.SPINNING_CIRCLE} />;
}
renderContent() {
const {expandedId, lastExpandedId} = this.state;
const {logs, theme, hide, isInitialLoading, isLoading, hasError, guildId} = this.props;
if (hide) {
return <StreamerModeEnabled />;
}
if (isLoading || isInitialLoading) {
return this.renderSpinner();
}
if (logs.length === 0) {
const body = hasError
? i18n.Messages.GUILD_SETTINGS_LABEL_AUDIT_LOG_ERROR_BODY
: i18n.Messages.GUILD_SETTINGS_LABEL_AUDIT_LOG_EMPTY_BODY;
const title = hasError
? i18n.Messages.GUILD_SETTINGS_LABEL_AUDIT_LOG_ERROR_TITLE
: i18n.Messages.GUILD_SETTINGS_LABEL_AUDIT_LOG_EMPTY_TITLE;
return (
<EmptyState theme={theme} className="margin-top-40">
<EmptyStateImage
darkSrc={require('../../images/empties/empty_server_settings_audit_log_dark.svg')}
lightSrc={require('../../images/empties/empty_server_settings_audit_log_light.svg')}
width={272}
height={130}
/>
<EmptyStateText note={body} style={{maxWidth: 300}}>
{title}
</EmptyStateText>
</EmptyState>
);
}
return logs.map(log => {
const expanded = expandedId === log.id;
const lastExpanded = lastExpandedId === log.id;
const ref = expanded ? this.handleSetExpandedRef : lastExpanded ? this.handleSetLastExpandedRef : null;
return (
<AuditLogClickWrap
guildId={guildId}
ref={ref}
className="margin-bottom-8"
onHeaderClick={this.handleHeaderClick}
onContentClick={this.handleContentClick}
log={log}
key={log.id}
expanded={expanded}
/>
);
});
}
renderLoadMore() {
const {showLoadMore, hasOlderLogs} = this.props;
if (showLoadMore && hasOlderLogs) {
return (
<Button color={Button.Colors.PRIMARY} className="margin-top-20" onClick={this.handleFetchNextPage}>
{i18n.Messages.GUILD_SETTINGS_AUDIT_LOG_LOAD_MORE}
</Button>
);
}
}
render() {
const {isLoadingNextPage, hide, isLoading, theme} = this.props;
const backgroundColor = theme === ThemeTypes.DARK ? Colors.PRIMARY_600 : Colors.WHITE;
return (
<div className="custom-column guild-settings-audit-logs">
<div className="custom-container">
<Scroller
backgroundColor={backgroundColor}
track
theme={Scroller.Themes.GHOST}
className="custom-scroller"
onScroll={this.handleOnScroll}
ref={this.handleSetScrollerRef}>
<Flex direction={Flex.Direction.VERTICAL} style={{paddingBottom: 60}}>
{this.renderHeader()}
{this.renderContent()}
{this.renderLoadMore()}
{isLoadingNextPage && !hide && !isLoading ? this.renderSpinner() : null}
</Flex>
</Scroller>
</div>
</div>
);
}
handleFilterActionChange({value}) {
PopoutActionCreators.close(ACTION_FILTER_POPOUT_ID);
filterByAction(value, this.props.guildId);
}
handleFilterUserChange({value}) {
PopoutActionCreators.close(USER_FILTER_POPOUT_ID);
filterByUserId(value, this.props.guildId);
}
handleHeaderClick(log: AuditLogRecord) {
const {expandedId} = this.state;
if (expandedId !== log.id) {
this._clickedInside = true;
this.setState({expandedId: log.id, lastExpandedId: expandedId});
} else {
this.setState({expandedId: null, lastExpandedId: null});
}
}
handleOutsideClick() {
if (this.state.expandedId != null && !this._clickedInside) {
this.setState({expandedId: null, lastExpandedId: null});
} else if (this.state.expandedId != null) {
this._clickedInside = false;
}
}
handleContentClick(e: Event) {
this._clickedInside = true;
e.stopPropagation();
}
handleSetScrollerRef(ref: Scroller) {
this._scrollerRef = ref;
}
handleOnScroll(scroller: Scroller) {
if (this.isScrollerAtBottom(scroller)) {
this.handleFetchNextPage();
}
}
handleFetchNextPage() {
fetchNextLogPage(this.props.guildId);
}
handleUserFilterQueryChange(query: string) {
this.setState({userFilterQuery: query});
PopoutActionCreators.rerender(USER_FILTER_POPOUT_ID);
}
handleUserFilterQueryClear() {
this.setState({userFilterQuery: ''});
PopoutActionCreators.rerender(USER_FILTER_POPOUT_ID);
}
handleActionFilterQueryChange(query: string) {
this.setState({actionFilterQuery: query});
PopoutActionCreators.rerender(ACTION_FILTER_POPOUT_ID);
}
handleActionFilterQueryClear() {
this.setState({actionFilterQuery: ''});
PopoutActionCreators.rerender(ACTION_FILTER_POPOUT_ID);
}
handleSetExpandedRef(ref: AuditLogClickWrap) {
this._expandedRef = ref;
}
handleSetLastExpandedRef(ref: AuditLogClickWrap) {
this._lastExpandedRef = ref;
}
}
export default Flux.connectStores(
[GuildSettingsStore, GuildSettingsAuditLogStore, GuildStore, ChannelStore, UserStore, EmojiStore, UserSettingsStore],
() => {
const guildId = GuildSettingsStore.getGuildId();
const guild = GuildStore.getGuild(guildId);
const logs = GuildSettingsAuditLogStore.logs;
const moderators = GuildSettingsAuditLogStore.userIds.map(id => UserStore.getUser(id));
return {
guildId,
moderators,
isInitialLoading: GuildSettingsAuditLogStore.isInitialLoading,
isLoading: GuildSettingsAuditLogStore.isLoading,
isLoadingNextPage: GuildSettingsAuditLogStore.isLoadingNextPage,
showLoadMore: GuildSettingsAuditLogStore.groupedFetchCount > MAX_GROUP_FETCH_NEXT_PAGES,
hasError: GuildSettingsAuditLogStore.hasError,
hasOlderLogs: GuildSettingsAuditLogStore.hasOlderLogs,
logs: logs != null && guild != null ? transformLogs(logs, guild) : [],
actionFilter: GuildSettingsAuditLogStore.actionFilter,
userIdFilter: GuildSettingsAuditLogStore.userIdFilter,
theme: UserSettingsStore.theme,
hide: StreamerModeStore.enabled,
};
}
)(GuildSettingsAuditLog);
// WEBPACK FOOTER //
// ./discord_app/components/guild_settings/GuildSettingsAuditLog.js