diff --git a/src/components/modlog.tsx b/src/components/modlog.tsx index 5d39eae7..be3a27ed 100644 --- a/src/components/modlog.tsx +++ b/src/components/modlog.tsx @@ -1,11 +1,6 @@ -import React, { Component } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { Link } from 'react-router-dom'; -import { Subscription } from 'rxjs'; -import { retryWhen, delay, take } from 'rxjs/operators'; import { - UserOperation, - GetModlogForm, - GetModlogResponse, ModRemovePost, ModLockPost, ModStickyPost, @@ -15,509 +10,534 @@ import { ModBan, ModAddCommunity, ModAdd, - WebSocketJsonResponse, - ModLogFilter, } from '../interfaces'; -import { WebSocketService } from '../services'; -import { - wsJsonToRes, - addTypeInfo, - fetchLimit, - toast, - siteName, -} from '../utils'; +import { fetchLimit, fetcher } from '../utils'; import { MomentTime } from './moment-time'; import moment from 'moment'; import { i18n } from '../i18next'; import { ModlogComment } from './mod-log-comment'; import Button from './elements/Button'; +import useSWR from 'swr'; +import { Box, Input, Select } from 'theme-ui'; +import { darken } from '@theme-ui/color'; -interface ModlogState { - combined: Array<{ - type_: string; - data: - | ModRemovePost - | ModLockPost - | ModStickyPost - | ModRemoveCommunity - | ModAdd - | ModBan; - }>; - communityId?: number; - communityName?: string; - page: number; - loading: boolean; - filters: ModLogFilter[]; -} - -export class Modlog extends Component { - private subscription: Subscription; - private emptyState: ModlogState = { - combined: [], - page: 1, - loading: true, - filters: Object.keys(ModLogFilter) as ModLogFilter[], - }; - - state = this.emptyState; +const filterNamesUI = [ + 'Removing Posts', + 'Locking Posts', + 'Stickying Posts', + 'Removing Comments', + 'Banning from Communities', + 'Adding Mod to Community', + 'Removing Communities', + 'Banning from Site', + 'Adding Mod to Site', +]; - componentDidMount() { - document.title = `Modlog - ${siteName}`; - const communityId = this.props.match.params.community_id - ? Number(this.props.match.params.community_id) - : undefined; - - this.state.communityId = communityId; - - this.subscription = WebSocketService.Instance.subject - .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) - .subscribe( - msg => this.parseMessage(msg), - err => console.error(err), - () => console.log('complete') - ); +function useClickOutsideHandler(refToCheck, refToControl) { + useEffect(() => { + function handleClickOutside(e) { + if (refToCheck.current && !refToCheck.current.contains(e.target)) { + refToControl.current.style.display = 'none'; + } + } - this.refetch(); - } + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }); +} - componentWillUnmount() { - this.subscription.unsubscribe(); +function generateNumFromBits(bits: Array): number { + let num = 0; + for (var i = 0; i < bits.length; i++) { + num += (bits[i] ? 1 : 0) << i; } + return num; +} - setCombined(res: GetModlogResponse) { - let removed_posts = addTypeInfo(res.removed_posts, 'removed_posts'); - let locked_posts = addTypeInfo(res.locked_posts, 'locked_posts'); - let stickied_posts = addTypeInfo(res.stickied_posts, 'stickied_posts'); - let removed_comments = addTypeInfo( - res.removed_comments, - 'removed_comments' - ); - let removed_communities = addTypeInfo( - res.removed_communities, - 'removed_communities' - ); - let banned_from_community = addTypeInfo( - res.banned_from_community, - 'banned_from_community' - ); - let added_to_community = addTypeInfo( - res.added_to_community, - 'added_to_community' - ); - let added = addTypeInfo(res.added, 'added'); - let banned = addTypeInfo(res.banned, 'banned'); - // this.state.combined = []; - const updatedState: any = { combined: [] }; +export function Modlog(props) { + const [[communityId, communityName], setCommunityInfo] = useState([ + props.match.params.community_id + ? Number(props.match.params.community_id) + : undefined, + undefined, + ]); + const [page, setPage] = useState(1); - updatedState.combined.push(...removed_posts); - updatedState.combined.push(...locked_posts); - updatedState.combined.push(...stickied_posts); - updatedState.combined.push(...removed_comments); - updatedState.combined.push(...removed_communities); - updatedState.combined.push(...banned_from_community); - updatedState.combined.push(...added_to_community); - updatedState.combined.push(...added); - updatedState.combined.push(...banned); + //array with length of filters instantiated to true + const [filtersOn, setFiltersOn] = useState( + new Array(filterNamesUI.length).fill(true) + ); - if (this.state.communityId && this.state.combined.length > 0) { - updatedState.communityName = (this.state.combined[0] - .data as ModRemovePost).community_name; - } + const [selectedMod, setSelectedMod] = useState(0); + const [selectedUser, setSelectedUser] = useState(0); - // Sort them by time - updatedState.combined.sort((a, b) => - b.data.when_.localeCompare(a.data.when_) - ); + const actionFilterDropdownRef = useRef(null); + useClickOutsideHandler(actionFilterDropdownRef, 'action-checkboxes'); - this.setState(updatedState); + const modlogQuery = useSWR( + () => + `/modlog?page=${page}${ + communityId ? `&community_id=${communityId}` : '' + }&limit=${fetchLimit}&action_filter=${generateNumFromBits(filtersOn)}${ + selectedMod == 0 ? '' : `&mod_user_id=${selectedMod}` + }${selectedUser == 0 ? '' : `&other_user_id=${selectedUser}`}`, + fetcher + ); + if (modlogQuery.error) { + console.log(modlogQuery.error); } - combined() { - return ( - - {this.state.combined - .filter(i => this.state.filters.includes(i.type_ as ModLogFilter)) - .map(i => ( - - - - - - - {i.data.mod_user_name} + return ( +
+
+
+
+
+ {communityName && ( + + /c/{communityName}{' '} - - - {i.type_ == 'removed_posts' && ( - <> - {(i.data as ModRemovePost).removed ? 'Removed' : 'Restored'} - - {' '} - Post{' '} - - {(i.data as ModRemovePost).post_name} - - -
- {(i.data as ModRemovePost).reason && - ` reason: ${(i.data as ModRemovePost).reason}`} -
- - )} - {i.type_ == 'locked_posts' && ( - <> - {(i.data as ModLockPost).locked ? 'Locked' : 'Unlocked'} - - {' '} - Post{' '} - - {(i.data as ModLockPost).post_name} - - - - )} - {i.type_ == 'stickied_posts' && ( - <> - {(i.data as ModStickyPost).stickied - ? 'Stickied' - : 'Unstickied'} - - {' '} - Post{' '} - - {(i.data as ModStickyPost).post_name} - - - - )} - {i.type_ == 'removed_comments' && ( - <> - {(i.data as ModRemoveComment).removed - ? 'Removed' - : 'Restored'} - - {' '} - Comment{' '} - - {(i.data as ModRemoveComment).comment_content.slice( - 0, - 100 - )} - {(i.data as ModRemoveComment).comment_content.length > - 100 && '...'} - - - {/* only show this expanding section for long comments */} - {(i.data as ModRemoveComment).comment_content.length > - 100 && ( - <> -
- - {(i.data as ModRemoveComment).comment_content} - - - )} - - {' '} - by{' '} - - {(i.data as ModRemoveComment).comment_user_name} - - -
- {(i.data as ModRemoveComment).reason && - ` reason: ${(i.data as ModRemoveComment).reason}`} -
- - )} - {i.type_ == 'removed_communities' && ( - <> - {(i.data as ModRemoveCommunity).removed - ? 'Removed' - : 'Restored'} - - {' '} - Community{' '} - - {(i.data as ModRemoveCommunity).community_name} - - -
- {(i.data as ModRemoveCommunity).reason && - ` reason: ${(i.data as ModRemoveCommunity).reason}`} -
-
- {(i.data as ModRemoveCommunity).expires && - ` expires: ${moment - .utc((i.data as ModRemoveCommunity).expires) - .fromNow()}`} -
- - )} - {i.type_ == 'banned_from_community' && ( - <> - - {(i.data as ModBanFromCommunity).banned - ? 'Banned ' - : 'Unbanned '}{' '} - - - - {(i.data as ModBanFromCommunity).other_user_name} - - - from the community - - - {(i.data as ModBanFromCommunity).community_name} - - -
- {(i.data as ModBanFromCommunity).reason && - ` reason: ${(i.data as ModBanFromCommunity).reason}`} -
-
- {(i.data as ModBanFromCommunity).expires && - ` expires: ${moment - .utc((i.data as ModBanFromCommunity).expires) - .fromNow()}`} -
- - )} - {i.type_ == 'added_to_community' && ( - <> - - {(i.data as ModAddCommunity).removed - ? 'Removed ' - : 'Appointed '}{' '} - - - - {(i.data as ModAddCommunity).other_user_name} - - - as a mod to the community - - - {(i.data as ModAddCommunity).community_name} - - - - )} - {i.type_ == 'banned' && ( - <> - - {(i.data as ModBan).banned ? 'Banned ' : 'Unbanned '}{' '} - - - - {(i.data as ModBan).other_user_name} - - -
- {(i.data as ModBan).reason && - ` reason: ${(i.data as ModBan).reason}`} -
-
- {(i.data as ModBan).expires && - ` expires: ${moment - .utc((i.data as ModBan).expires) - .fromNow()}`} -
- - )} - {i.type_ == 'added' && ( - <> - - {(i.data as ModAdd).removed ? 'Removed ' : 'Appointed '}{' '} - - - - {(i.data as ModAdd).other_user_name} - - - as an admin - - )} - - - ))} - - ); - } - - render() { - return ( -
- {this.state.loading ? ( -
- - - -
- ) : ( -
-
+ )} + {i18n.t('modlog')} +
+
+ {i18n.t('modlog_warning')} +
+
+
+
{ + let checkboxes = document.getElementById('action-checkboxes'); + if (checkboxes.style.display == 'none') { + checkboxes.style.display = 'block'; + } else { + checkboxes.style.display = 'none'; + } }} > - {Object.keys(ModLogFilter).map(filter => ( -
+ +
+
+ + {filterNamesUI.map((filter, index) => ( +
))} -
-
-
- {this.state.communityName && ( - - /c/{this.state.communityName}{' '} - - )} - {i18n.t('modlog')} -
-
-
-
- {i18n.t('modlog_warning')} -
-
+
-
- - - - - - - - - {this.combined()} -
{i18n.t('time')}{i18n.t('mod')}{i18n.t('action')}
- {this.paginator()} + + +
+
+ {!modlogQuery.data ? ( +
+ + + +
+ ) : ( +
+ +
+ {page > 1 && ( + + )} +
)}
- ); - } - - paginator() { - return ( -
- {this.state.page > 1 && ( - - )} - -
- ); - } +
+ ); +} - nextPage = () => { - this.setState({ page: this.state.page + 1 }); - this.refetch(); - }; +function UserFilter(props) { + const [searchList, setSearchList] = useState([]); + const [searchForm, setSearchForm] = useState(''); + const [showBanned, setShowBanned] = useState(false); - handleFilterChange = e => { - const name = e.target.name; - const { filters } = this.state; + const entireRef = useRef(null); + const dropdownRef = useRef(null); + useClickOutsideHandler(entireRef, dropdownRef); - if (filters.includes(name)) { - this.setState({ - filters: [ - ...this.state.filters.filter(filterName => filterName !== name), - ], - }); - } else { - this.setState({ filters: [...this.state.filters, name] }); + const searchQuery = useSWR( + searchForm.length > 2 + ? `/search?q=${searchForm}&type_=Users&sort=Active` + : null, + fetcher, + { + initialData: {}, + onSuccess: data => { + setSearchList(data.users); + //console.log(data.users); + }, } - }; - - prevPage = () => { - this.setState({ page: Math.max(this.state.page - 1, 0) }); - this.refetch(); - }; + ); - refetch() { - let modlogForm: GetModlogForm = { - community_id: this.state.communityId, - page: this.state.page, - limit: fetchLimit, - }; - WebSocketService.Instance.getModlog(modlogForm); - } + return ( +
+ (dropdownRef.current.style.display = 'block')} + onChange={e => { + const targetVal = (e.target as HTMLInputElement).value; + props.setSelectedUser(0); + if (targetVal.length < 3) { + setSearchList([]); + } else if (targetVal.length < 5 && searchList.length == 0) { + searchQuery.revalidate(); + } + setSearchForm(targetVal); + }} + /> + +
+

+ Show banned users +

+ setShowBanned(!showBanned)} + /> +
+ {searchList.filter( + user => + user.name.toLowerCase().includes(searchForm.toLowerCase()) && + (!props.mod || user.admin || user.sitemod || user.moderator) && + (showBanned || !user.banned) + ).length == 0 ? ( + {searchForm.length < 4 ? 'Type more...' : 'No matches found'} + ) : ( + searchList + .filter( + user => + user.name.toLowerCase().includes(searchForm.toLowerCase()) && + (!props.mod || user.admin || user.sitemod || user.moderator) && + (showBanned || !user.banned) + ) + .map(user => ( +
+ +
+ )) + )} +
+
+ ); +} - parseMessage(msg: WebSocketJsonResponse) { - console.log(msg); - let res = wsJsonToRes(msg); - if (msg.error) { - toast(i18n.t(msg.error), 'danger'); - return; - } else if (res.op == UserOperation.GetModlog) { - let data = res.data as GetModlogResponse; - this.setState({ loading: false }); - window.scrollTo(0, 0); - this.setCombined(data); - } - } +function ActionList(props) { + return ( + + + + + + + + + + {props.actions.map(i => ( + + + + + + ))} + +
{i18n.t('time')}{i18n.t('mod')}{i18n.t('action')}
+ + + {i.mod_user_name} + + {i.type == 'RemovePost' && ( + <> + {(i as ModRemovePost).removed ? 'Removed' : 'Restored'} + + {' '} + Post{' '} + + {(i as ModRemovePost).post_name} + + + + {' '} + by{' '} + + {(i as ModRemovePost).other_user_name} + + +
+ {(i as ModRemovePost).reason && + ` reason: ${(i as ModRemovePost).reason}`} +
+ + )} + {i.type == 'LockPost' && ( + <> + {(i as ModLockPost).locked ? 'Locked' : 'Unlocked'} + + {' '} + Post{' '} + + {(i as ModLockPost).post_name} + + + + {' '} + by{' '} + + {(i as ModLockPost).other_user_name} + + + + )} + {i.type == 'StickyPost' && ( + <> + {(i as ModStickyPost).stickied ? 'Stickied' : 'Unstickied'} + + {' '} + Post{' '} + + {(i as ModStickyPost).post_name} + + + + {' '} + by{' '} + + {(i as ModStickyPost).other_user_name} + + + + )} + {i.type == 'RemoveComment' && ( + <> + {(i as ModRemoveComment).removed ? 'Removed' : 'Restored'} + + {' '} + Comment{' '} + + {(i as ModRemoveComment).comment_content.slice(0, 100)} + {(i as ModRemoveComment).comment_content.length > 100 && + '...'} + + + {/* only show this expanding section for long comments */} + {(i as ModRemoveComment).comment_content.length > 100 && ( + <> +
+ + {(i as ModRemoveComment).comment_content} + + + )} + + {' '} + by{' '} + + {(i as ModRemoveComment).comment_user_name} + + +
+ {(i as ModRemoveComment).reason && + ` reason: ${(i as ModRemoveComment).reason}`} +
+ + )} + {i.type == 'RemoveCommunity' && ( + <> + {(i as ModRemoveCommunity).removed ? 'Removed' : 'Restored'} + + {' '} + Community{' '} + + {(i as ModRemoveCommunity).community_name} + + +
+ {(i as ModRemoveCommunity).reason && + ` reason: ${(i as ModRemoveCommunity).reason}`} +
+
+ {(i as ModRemoveCommunity).expires && + ` expires: ${moment + .utc((i as ModRemoveCommunity).expires) + .fromNow()}`} +
+ + )} + {i.type == 'BanFromCommunity' && ( + <> + + {(i as ModBanFromCommunity).banned + ? 'Banned ' + : 'Unbanned '}{' '} + + + + {(i as ModBanFromCommunity).other_user_name} + + + from the community + + + {(i as ModBanFromCommunity).community_name} + + +
+ {(i as ModBanFromCommunity).reason && + ` reason: ${(i as ModBanFromCommunity).reason}`} +
+
+ {(i as ModBanFromCommunity).expires && + ` expires: ${moment + .utc((i as ModBanFromCommunity).expires) + .fromNow()}`} +
+ + )} + {i.type == 'AddModToCommunity' && ( + <> + + {(i as ModAddCommunity).removed ? 'Removed ' : 'Appointed '}{' '} + + + + {(i as ModAddCommunity).other_user_name} + + + as a mod to the community + + + {(i as ModAddCommunity).community_name} + + + + )} + {i.type == 'BanFromSite' && ( + <> + {(i as ModBan).banned ? 'Banned ' : 'Unbanned '} + + + {(i as ModBan).other_user_name} + + +
+ {(i as ModBan).reason && ` reason: ${(i as ModBan).reason}`} +
+
+ {(i as ModBan).expires && + ` expires: ${moment + .utc((i as ModBan).expires) + .fromNow()}`} +
+ + )} + {i.type == 'AddMod' && ( + <> + + {(i as ModAdd).removed ? 'Removed ' : 'Appointed '}{' '} + + + + {(i as ModAdd).other_user_name} + + + as an admin + + )} +
+ ); } diff --git a/src/custom.css b/src/custom.css index a2839750..99d5364c 100644 --- a/src/custom.css +++ b/src/custom.css @@ -989,6 +989,33 @@ a:hover { align-items: center; } +.modlog-dropdown { + display: none; + position: absolute; + width: 100%; + max-height: 400px; + overflow: auto; + padding: 10px; + border-width: 1px; + border-radius: 3px; + z-index: 2; +} + +.modlog-filter-input { + position: relative; + width: 15%; + min-width: 200px; + margin: 2px 5px 2px 5px; +} + +.mod-action-overselect { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; +} + /* This code is subject to LICENSE in root of this repository */ /* Used to detect in JavaScript if apps have loaded styles or not. */ diff --git a/src/interfaces.ts b/src/interfaces.ts index f32ebf90..cad7320e 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -537,11 +537,13 @@ export interface GetModlogResponse { export interface ModRemovePost { id: number; mod_user_id: number; + other_user_id: number; post_id: number; reason?: string; removed?: boolean; when_: string; mod_user_name: string; + other_user_name: string; post_name: string; community_id: number; community_name: string; @@ -550,10 +552,12 @@ export interface ModRemovePost { export interface ModLockPost { id: number; mod_user_id: number; + other_user_id: number; post_id: number; locked?: boolean; when_: string; mod_user_name: string; + other_user_name: string; post_name: string; community_id: number; community_name: string; @@ -562,10 +566,12 @@ export interface ModLockPost { export interface ModStickyPost { id: number; mod_user_id: number; + other_user_id: number; post_id: number; stickied?: boolean; when_: string; mod_user_name: string; + other_user_name: string; post_name: string; community_id: number; community_name: string;