945 lines
31 KiB
JavaScript
Executable File
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
|