You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

544 lines
18 KiB

import React, { useEffect, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import {
ModRemovePost,
ModLockPost,
ModStickyPost,
ModRemoveComment,
ModRemoveCommunity,
ModBanFromCommunity,
ModBan,
ModAddCommunity,
ModAdd,
} from '../interfaces';
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';
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',
];
function useClickOutsideHandler(refToCheck, refToControl) {
useEffect(() => {
function handleClickOutside(e) {
if (refToCheck.current && !refToCheck.current.contains(e.target)) {
refToControl.current.style.display = 'none';
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
});
}
function generateNumFromBits(bits: Array<boolean>): number {
let num = 0;
for (var i = 0; i < bits.length; i++) {
num += (bits[i] ? 1 : 0) << i;
}
return num;
}
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);
//array with length of filters instantiated to true
const [filtersOn, setFiltersOn] = useState(
new Array(filterNamesUI.length).fill(true)
);
const [selectedMod, setSelectedMod] = useState(0);
const [selectedUser, setSelectedUser] = useState(0);
const actionFilterDropdownRef = useRef(null);
const actionFilterWindowRef = useRef(null);
useClickOutsideHandler(actionFilterDropdownRef, actionFilterWindowRef);
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);
}
return (
<div className="container">
<div>
<div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
}}
>
<h5
style={{
float: 'left',
padding: '5px',
}}
>
{communityName && (
<Link className="text-body" to={`/c/${communityName}`}>
/c/{communityName}{' '}
</Link>
)}
<span>{i18n.t('modlog')}</span>
</h5>
<h6
style={{
border: '3px solid #000000',
padding: '5px',
maxWidth: '70%',
backgroundColor: '#F39C12',
color: '#111111',
float: 'right',
}}
>
{i18n.t('modlog_warning')}
</h6>
</div>
<div
style={{
display: 'inline-flex',
flexWrap: 'wrap',
margin: '5px',
width: '100%',
}}
>
<div ref={actionFilterDropdownRef} className="modlog-filter-input">
<div
onClick={() => {
let checkboxes = actionFilterWindowRef.current;
if (checkboxes.style.display == 'none') {
checkboxes.style.display = 'block';
} else {
checkboxes.style.display = 'none';
}
}}
>
<Select style={{ width: '100%' }}>
<option>Filter by action</option>
</Select>
<div
className="mod-action-overselect"
style={{ cursor: 'pointer' }}
/>
</div>
<Box
className="modlog-dropdown"
ref={actionFilterWindowRef}
sx={{ bg: darken('background', 0.03) }}
>
{filterNamesUI.map((filter, index) => (
<div className="form-check" key={filter}>
<label className="form-check-label mod-log-filter-label">
<input
className="form-check-input"
type="checkbox"
name={filter}
checked={filtersOn[index]}
onChange={() => {
let tempFilters = [...filtersOn];
tempFilters[index] = !tempFilters[index];
setFiltersOn(tempFilters);
modlogQuery.revalidate(); //query modlog again since filters changed
}}
/>
{filter.replace(/_/g, ' ')}
</label>
</div>
))}
</Box>
</div>
<UserFilter mod setSelectedUser={setSelectedMod} />
<UserFilter mod={false} setSelectedUser={setSelectedUser} />
</div>
</div>
{!modlogQuery.data ? (
<h5>
<svg className="icon icon-spinner spin">
<use xlinkHref="#icon-spinner" />
</svg>
</h5>
) : (
<div className="table-responsive">
<ActionList actions={modlogQuery.data.log} filtersOn={filtersOn} />
<div className="mt-2">
{page > 1 && (
<Button
className="btn btn-sm btn-secondary mr-1"
onClick={() => setPage(Math.max(page - 1, 1))}
>
{i18n.t('prev')}
</Button>
)}
<Button
className="btn btn-sm btn-secondary"
onClick={() => setPage(page + 1)}
>
{i18n.t('next')}
</Button>
</div>
</div>
)}
</div>
</div>
);
}
function UserFilter(props) {
const [searchList, setSearchList] = useState([]);
const [searchForm, setSearchForm] = useState('');
const [showBanned, setShowBanned] = useState(false);
const entireRef = useRef(null);
const dropdownRef = useRef(null);
useClickOutsideHandler(entireRef, dropdownRef);
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);
},
}
);
return (
<div ref={entireRef} className="modlog-filter-input">
<Input
value={searchForm}
placeholder={props.mod ? 'Filter by mod' : 'Filter by user'}
style={{ minWidth: '200px' }}
onFocus={() => (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);
}}
/>
<Box
ref={dropdownRef}
className="modlog-dropdown"
sx={{ bg: darken('background', 0.03) }}
>
<div style={{ height: '30px' }}>
<p style={{ color: '#999', fontSize: '14px', paddingRight: '8px' }}>
Show banned users
<input
type="checkbox"
checked={showBanned}
style={{ scale: '0.8', marginLeft: '5px' }}
onChange={() => setShowBanned(!showBanned)}
/>
</p>
</div>
{searchList.filter(
user =>
user.name.toLowerCase().includes(searchForm.toLowerCase()) &&
(!props.mod || user.admin || user.sitemod || user.moderator) &&
(showBanned || !user.banned)
).length == 0 ? (
<i>{searchForm.length < 4 ? 'Type more...' : 'No matches found'}</i>
) : (
searchList
.filter(
user =>
user.name.toLowerCase().includes(searchForm.toLowerCase()) &&
(!props.mod || user.admin || user.sitemod || user.moderator) &&
(showBanned || !user.banned)
)
.map(user => (
<div key={user.id}>
<button
className="btn btn-secondary btn-sm"
style={{ margin: '3px 0px 3px 0px' }}
onClick={() => {
props.setSelectedUser(user.id);
setSearchForm(user.name);
dropdownRef.current.style.display = 'none';
}}
>
{user.name}
</button>
</div>
))
)}
</Box>
</div>
);
}
function ActionList(props) {
return (
<table id="modlog_table" className="table table-sm table-hover">
<thead className="pointer">
<tr>
<th> {i18n.t('time')}</th>
<th>{i18n.t('mod')}</th>
<th>{i18n.t('action')}</th>
</tr>
</thead>
<tbody>
{props.actions.map(i => (
<tr key={`${i.type}-${i.id}`}>
<td>
<MomentTime data={i} />
</td>
<td>
<Link to={`/u/${i.mod_user_name}`}>{i.mod_user_name}</Link>
</td>
<td>
{i.type == 'RemovePost' && (
<>
{(i as ModRemovePost).removed ? 'Removed' : 'Restored'}
<span>
{' '}
Post{' '}
<Link to={`/post/${(i as ModRemovePost).post_id}`}>
{(i as ModRemovePost).post_name}
</Link>
</span>
<span>
{' '}
by{' '}
<Link to={`/u/${(i as ModRemovePost).other_user_name}`}>
{(i as ModRemovePost).other_user_name}
</Link>
</span>
<div>
{(i as ModRemovePost).reason &&
` reason: ${(i as ModRemovePost).reason}`}
</div>
</>
)}
{i.type == 'LockPost' && (
<>
{(i as ModLockPost).locked ? 'Locked' : 'Unlocked'}
<span>
{' '}
Post{' '}
<Link to={`/post/${(i as ModLockPost).post_id}`}>
{(i as ModLockPost).post_name}
</Link>
</span>
<span>
{' '}
by{' '}
<Link to={`/u/${(i as ModLockPost).other_user_name}`}>
{(i as ModLockPost).other_user_name}
</Link>
</span>
</>
)}
{i.type == 'StickyPost' && (
<>
{(i as ModStickyPost).stickied ? 'Stickied' : 'Unstickied'}
<span>
{' '}
Post{' '}
<Link to={`/post/${(i as ModStickyPost).post_id}`}>
{(i as ModStickyPost).post_name}
</Link>
</span>
<span>
{' '}
by{' '}
<Link to={`/u/${(i as ModStickyPost).other_user_name}`}>
{(i as ModStickyPost).other_user_name}
</Link>
</span>
</>
)}
{i.type == 'RemoveComment' && (
<>
{(i as ModRemoveComment).removed ? 'Removed' : 'Restored'}
<span>
{' '}
Comment{' '}
<Link
to={`/post/${(i as ModRemoveComment).post_id}/comment/${
(i as ModRemoveComment).comment_id
}`}
>
{(i as ModRemoveComment).comment_content.slice(0, 100)}
{(i as ModRemoveComment).comment_content.length > 100 &&
'...'}
</Link>
</span>
{/* only show this expanding section for long comments */}
{(i as ModRemoveComment).comment_content.length > 100 && (
<>
<br />
<ModlogComment>
{(i as ModRemoveComment).comment_content}
</ModlogComment>
</>
)}
<span>
{' '}
by{' '}
<Link
to={`/u/${(i as ModRemoveComment).comment_user_name}`}
>
{(i as ModRemoveComment).comment_user_name}
</Link>
</span>
<div>
{(i as ModRemoveComment).reason &&
` reason: ${(i as ModRemoveComment).reason}`}
</div>
</>
)}
{i.type == 'RemoveCommunity' && (
<>
{(i as ModRemoveCommunity).removed ? 'Removed' : 'Restored'}
<span>
{' '}
Community{' '}
<Link to={`/c/${(i as ModRemoveCommunity).community_name}`}>
{(i as ModRemoveCommunity).community_name}
</Link>
</span>
<div>
{(i as ModRemoveCommunity).reason &&
` reason: ${(i as ModRemoveCommunity).reason}`}
</div>
<div>
{(i as ModRemoveCommunity).expires &&
` expires: ${moment
.utc((i as ModRemoveCommunity).expires)
.fromNow()}`}
</div>
</>
)}
{i.type == 'BanFromCommunity' && (
<>
<span>
{(i as ModBanFromCommunity).banned
? 'Banned '
: 'Unbanned '}{' '}
</span>
<span>
<Link
to={`/u/${(i as ModBanFromCommunity).other_user_name}`}
>
{(i as ModBanFromCommunity).other_user_name}
</Link>
</span>
<span> from the community </span>
<span>
<Link
to={`/c/${(i as ModBanFromCommunity).community_name}`}
>
{(i as ModBanFromCommunity).community_name}
</Link>
</span>
<div>
{(i as ModBanFromCommunity).reason &&
` reason: ${(i as ModBanFromCommunity).reason}`}
</div>
<div>
{(i as ModBanFromCommunity).expires &&
` expires: ${moment
.utc((i as ModBanFromCommunity).expires)
.fromNow()}`}
</div>
</>
)}
{i.type == 'AddModToCommunity' && (
<>
<span>
{(i as ModAddCommunity).removed ? 'Removed ' : 'Appointed '}{' '}
</span>
<span>
<Link to={`/u/${(i as ModAddCommunity).other_user_name}`}>
{(i as ModAddCommunity).other_user_name}
</Link>
</span>
<span> as a mod to the community </span>
<span>
<Link to={`/c/${(i as ModAddCommunity).community_name}`}>
{(i as ModAddCommunity).community_name}
</Link>
</span>
</>
)}
{i.type == 'BanFromSite' && (
<>
<span>{(i as ModBan).banned ? 'Banned ' : 'Unbanned '} </span>
<span>
<Link to={`/u/${(i as ModBan).other_user_name}`}>
{(i as ModBan).other_user_name}
</Link>
</span>
<div>
{(i as ModBan).reason && ` reason: ${(i as ModBan).reason}`}
</div>
<div>
{(i as ModBan).expires &&
` expires: ${moment
.utc((i as ModBan).expires)
.fromNow()}`}
</div>
</>
)}
{i.type == 'AddMod' && (
<>
<span>
{(i as ModAdd).removed ? 'Removed ' : 'Appointed '}{' '}
</span>
<span>
<Link to={`/u/${(i as ModAdd).other_user_name}`}>
{(i as ModAdd).other_user_name}
</Link>
</span>
<span> as an admin </span>
</>
)}
</td>
</tr>
))}
</tbody>
</table>
);
}