Flag users as "Always premoderate" (#2145)

* Flag users as "Always premoderate"

Status can be set using the action dropdown in the People tab and in User Details.
Users flagged as "Always premoderate" will have their comments sent to the premod queue.
Users can be filtered with the Always Premoderate status.
Include Always Premoderate status changes in User History.
Add spanish translations.

* Reorder CSS as per cvle's suggestion

* Address second comment
This commit is contained in:
Gerardo Gálvez
2019-02-13 20:08:22 +01:00
committed by Kiwi
parent 19b425dc1a
commit 8163b52301
38 changed files with 714 additions and 7 deletions
+2 -1
View File
@@ -105,7 +105,8 @@ function printUserAsTable(user) {
Suspension: user.suspended
? `Until ${user.status.suspension.until}`
: false,
}
},
{ 'Always premod comments': user.alwaysPremod }
);
console.log(table.toString());
@@ -0,0 +1,14 @@
import {
SHOW_ALWAYS_PREMOD_USER_DIALOG,
HIDE_ALWAYS_PREMOD_USER_DIALOG,
} from '../constants/alwaysPremodUserDialog.js';
export const showAlwaysPremodUserDialog = ({ userId, username }) => ({
type: SHOW_ALWAYS_PREMOD_USER_DIALOG,
userId,
username,
});
export const hideAlwaysPremodUserDialog = () => ({
type: HIDE_ALWAYS_PREMOD_USER_DIALOG,
});
@@ -9,6 +9,8 @@ import {
SET_SEARCH_VALUE,
SHOW_BANUSER_DIALOG,
HIDE_BANUSER_DIALOG,
SHOW_ALWAYS_PREMOD_USER_DIALOG,
HIDE_ALWAYS_PREMOD_USER_DIALOG,
SHOW_REJECT_USERNAME_DIALOG,
HIDE_REJECT_USERNAME_DIALOG,
SET_INDICATOR_TRACK,
@@ -61,6 +63,15 @@ export const setSearchValue = value => ({
export const showBanUserDialog = user => ({ type: SHOW_BANUSER_DIALOG, user });
export const hideBanUserDialog = () => ({ type: HIDE_BANUSER_DIALOG });
// Always premod User Dialog
export const showAlwaysPremodUserDialog = user => ({
type: SHOW_ALWAYS_PREMOD_USER_DIALOG,
user,
});
export const hideAlwaysPremodUserDialog = () => ({
type: HIDE_ALWAYS_PREMOD_USER_DIALOG,
});
// Reject Username Dialog
export const showRejectUsernameDialog = user => ({
type: SHOW_REJECT_USERNAME_DIALOG,
@@ -0,0 +1,29 @@
.dialog {
border: none;
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
width: 400px;
top: 50%;
transform: translateY(-50%);
padding: 20px;
border-radius: 4px;
}
.header {
color: black;
font-size: 1.5em;
font-weight: 500;
margin: 0 0 8px 0;
}
.subheader {
color: black;
font-size: 1.3em;
font-weight: 500;
margin: 0 0 8px 0;
}
.buttons {
margin-top: 8px;
margin-bottom: 6px;
text-align: right;
}
@@ -0,0 +1,68 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import { Dialog } from 'coral-ui';
import styles from './AlwaysPremodUserDialog.css';
import Button from 'coral-ui/components/Button';
import t from 'coral-framework/services/i18n';
class AlwaysPremodUserDialog extends React.Component {
handlePerform = () => {
this.props.onPerform();
};
render() {
const { open, onCancel, username, info } = this.props;
return (
<Dialog
className={cn(styles.dialog, 'talk-always-premod-user-dialog')}
id="alwaysPremodUserDialog"
open={open}
onCancel={onCancel}
title={t('alwayspremoddialog.always_premod_user')}
>
<span className={styles.close} onClick={onCancel}>
×
</span>
<section>
<h2 className={styles.header}>
{t('alwayspremoddialog.always_premod_user')}
</h2>
<h3 className={styles.subheader}>
{t('alwayspremoddialog.are_you_sure', username)}
</h3>
<p className={styles.description}>{info}</p>
<div className={styles.buttons}>
<Button
className={cn('talk-always-premod-user-dialog-button-cancel')}
cStyle="white"
onClick={onCancel}
raised
>
{t('alwayspremoddialog.cancel')}
</Button>
<Button
className={cn('talk-always-premod-user-dialog-button-confirm')}
cStyle="black"
onClick={this.handlePerform}
raised
>
{t('alwayspremoddialog.yes_always_premod_user')}
</Button>
</div>
</section>
</Dialog>
);
}
}
AlwaysPremodUserDialog.propTypes = {
open: PropTypes.bool,
onPerform: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
username: PropTypes.string,
info: PropTypes.string,
};
export default AlwaysPremodUserDialog;
@@ -137,6 +137,12 @@
display: inline-block;
}
.actionsMenuAlwaysPremod {
background-color: #F0B50B;
border-color: #F0B50B;
color: white;
}
.actionsMenuSuspended {
background-color: #F29336;
border-color: #F29336;
@@ -12,6 +12,7 @@ import {
isUsernameRejected,
isUsernameChanged,
isBanned,
isAlwaysPremod,
getKarma,
} from 'coral-framework/utils/user';
@@ -45,6 +46,7 @@ function getUserStatusArray(user) {
const statusMap = {
suspended: isSuspended,
banned: isBanned,
alwaysPremod: isAlwaysPremod,
usernameRejected: isUsernameRejected,
usernameChanged: isUsernameChanged,
};
@@ -68,6 +70,12 @@ class UserDetail extends React.Component {
username: this.props.root.user.username,
});
showAlwaysPremodUserDialog = () =>
this.props.showAlwaysPremodUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
showRejectUsernameDialog = () =>
this.props.showRejectUsernameDialog({
userId: this.props.root.user.id,
@@ -107,6 +115,8 @@ class UserDetail extends React.Component {
return t('user_detail.suspended');
case 'banned':
return t('user_detail.banned');
case 'alwaysPremod':
return t('user_detail.always_premoded');
case 'usernameRejected':
return (
<span>
@@ -153,6 +163,7 @@ class UserDetail extends React.Component {
toggleSelectAll,
unbanUser,
unsuspendUser,
removeAlwaysPremodUser,
modal,
acceptComment,
rejectComment,
@@ -169,6 +180,7 @@ class UserDetail extends React.Component {
const banned = isBanned(user);
const suspended = isSuspended(user);
const alwaysPremod = isAlwaysPremod(user);
const usernameRejected = isUsernameRejected(user);
const usernameChanged = isUsernameChanged(user);
@@ -200,6 +212,7 @@ class UserDetail extends React.Component {
{
[styles.actionsMenuSuspended]: suspended,
[styles.actionsMenuBanned]: banned,
[styles.actionsMenuAlwaysPremod]: alwaysPremod,
},
'talk-admin-user-detail-actions-button'
)}
@@ -231,6 +244,21 @@ class UserDetail extends React.Component {
</ActionsMenuItem>
)}
{alwaysPremod ? (
<ActionsMenuItem
onClick={() => removeAlwaysPremodUser({ id: user.id })}
>
{t('user_detail.remove_always_premod')}
</ActionsMenuItem>
) : (
<ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showAlwaysPremodUserDialog}
>
{t('user_detail.always_premod')}
</ActionsMenuItem>
)}
{usernameChanged && (
<ActionsMenuItem onClick={this.goToReportedUsernames}>
{t('user_detail.username_needs_approval')}
@@ -476,8 +504,10 @@ UserDetail.propTypes = {
showRejectUsernameDialog: PropTypes.func,
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
showAlwaysPremodUserDialog: PropTypes.func,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
removeAlwaysPremodUser: PropTypes.func.isRequired,
modal: PropTypes.bool,
rejectUsername: PropTypes.func.isRequired,
};
@@ -48,6 +48,10 @@ const buildActionResponse = (typename, created_at, until, status) => {
return status
? t('user_history.user_banned')
: t('user_history.ban_removed');
case 'AlwaysPremodStatusHistory':
return status
? t('user_history.user_always_premoded')
: t('user_history.always_premod_removed');
case 'SuspensionStatusHistory':
return until
? t('user_history.suspended', readableDuration(created_at, until))
@@ -0,0 +1,2 @@
export const SHOW_ALWAYS_PREMOD_USER_DIALOG = 'SHOW_ALWAYS_PREMOD_USER_DIALOG';
export const HIDE_ALWAYS_PREMOD_USER_DIALOG = 'HIDE_ALWAYS_PREMOD_USER_DIALOG';
@@ -14,6 +14,9 @@ export const FETCH_FLAGGED_COMMENTERS_FAILURE = `${prefix}_FETCH_FLAGGED_COMMENT
export const SHOW_BANUSER_DIALOG = `${prefix}_SHOW_BANUSER_DIALOG`;
export const HIDE_BANUSER_DIALOG = `${prefix}_HIDE_BANUSER_DIALOG`;
export const SHOW_ALWAYS_PREMOD_USER_DIALOG = `${prefix}_SHOW_ALWAYS_PREMOD_USER_DIALOG`;
export const HIDE_ALWAYS_PREMOD_USER_DIALOG = `${prefix}_HIDE_ALWAYS_PREMOD_USER_DIALOG`;
export const SHOW_REJECT_USERNAME_DIALOG = `${prefix}_SHOW_REJECT_USERNAME_DIALOG`;
export const HIDE_REJECT_USERNAME_DIALOG = `${prefix}_HIDE_REJECT_USERNAME_DIALOG`;
@@ -0,0 +1,66 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import AlwaysPremodUserDialog from '../components/AlwaysPremodUserDialog';
import { hideAlwaysPremodUserDialog } from '../actions/alwaysPremodUserDialog';
import { withAlwaysPremodUser } from 'coral-framework/graphql/mutations';
import { compose } from 'react-apollo';
import t from 'coral-framework/services/i18n';
class AlwaysPremodUserDialogContainer extends Component {
alwaysPremodUser = async () => {
const { userId, alwaysPremodUser, hideAlwaysPremodUserDialog } = this.props;
await alwaysPremodUser({ id: userId });
hideAlwaysPremodUserDialog();
};
getInfo() {
let note = t('alwayspremoddialog.note_always_premod_user');
return t('alwayspremoddialog.note', note);
}
render() {
return (
<AlwaysPremodUserDialog
open={this.props.open}
onPerform={this.alwaysPremodUser}
onCancel={this.props.hideAlwaysPremodUserDialog}
username={this.props.username}
info={this.getInfo()}
/>
);
}
}
AlwaysPremodUserDialogContainer.propTypes = {
alwaysPremodUser: PropTypes.func.isRequired,
hideAlwaysPremodUserDialog: PropTypes.func,
open: PropTypes.bool,
username: PropTypes.string,
};
const mapStateToProps = ({
alwaysPremodUserDialog: { open, userId, username },
}) => ({
open,
userId,
username,
});
const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
hideAlwaysPremodUserDialog,
},
dispatch
),
});
export default compose(
connect(
mapStateToProps,
mapDispatchToProps
),
withAlwaysPremodUser
)(AlwaysPremodUserDialogContainer);
@@ -5,6 +5,7 @@ import Layout from '../components/Layout';
import Login from '../containers/Login';
import { FullLoading } from '../components/FullLoading';
import BanUserDialog from './BanUserDialog';
import AlwaysPremodUserDialog from './AlwaysPremodUserDialog';
import SuspendUserDialog from './SuspendUserDialog';
import RejectUsernameDialog from './RejectUsernameDialog';
import { toggleModal as toggleShortcutModal } from '../actions/moderation';
@@ -40,6 +41,7 @@ class LayoutContainer extends React.Component {
toggleShortcutModal={toggleShortcutModal}
currentUser={this.props.currentUser}
>
<AlwaysPremodUserDialog />
<BanUserDialog />
<SuspendUserDialog />
<RejectUsernameDialog />
@@ -22,12 +22,14 @@ import {
withSetCommentStatus,
withUnbanUser,
withUnsuspendUser,
withRemoveAlwaysPremodUser,
withRejectUsername,
withPostFlag,
} from 'coral-framework/graphql/mutations';
import UserDetailComment from './UserDetailComment';
import update from 'immutability-helper';
import { showBanUserDialog } from 'actions/banUserDialog';
import { showAlwaysPremodUserDialog } from 'actions/alwaysPremodUserDialog';
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
import { showRejectUsernameDialog } from 'actions/rejectUsernameDialog';
@@ -153,6 +155,7 @@ UserDetailContainer.propTypes = {
selectedCommentIds: PropTypes.array,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
removeAlwaysPremodUser: PropTypes.func.isRequired,
rejectUsername: PropTypes.func.isRequired,
userId: PropTypes.string,
};
@@ -218,6 +221,17 @@ export const withUserDetailQuery = withQuery(
created_at
}
}
alwaysPremod {
status
history {
status
assigned_by {
id
username
}
created_at
}
},
username {
status
history {
@@ -280,6 +294,7 @@ const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
showBanUserDialog,
showAlwaysPremodUserDialog,
showSuspendUserDialog,
showRejectUsernameDialog,
changeTab,
@@ -302,6 +317,7 @@ export default compose(
withSetCommentStatus,
withUnbanUser,
withUnsuspendUser,
withRemoveAlwaysPremodUser,
withRejectUsername,
withPostFlag,
withRouter
+78
View File
@@ -10,6 +10,9 @@ const userStatusFragment = gql`
banned {
status
}
alwaysPremod {
status
}
suspension {
until
}
@@ -125,6 +128,64 @@ export default {
});
},
}),
AlwaysPremodUser: ({
variables: {
input: { id },
},
}) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
fragment: userStatusFragment,
id: fragmentId,
});
const updated = update(data, {
state: {
status: {
alwaysPremod: {
status: { $set: true },
},
},
},
});
proxy.writeFragment({
fragment: userStatusFragment,
id: fragmentId,
data: updated,
});
},
}),
RemoveAlwaysPremodUser: ({
variables: {
input: { id },
},
}) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
fragment: userStatusFragment,
id: fragmentId,
});
const updated = update(data, {
state: {
status: {
alwaysPremod: {
status: { $set: false },
},
},
},
});
proxy.writeFragment({
fragment: userStatusFragment,
id: fragmentId,
data: updated,
});
},
}),
BanUser: ({
variables: {
input: { id },
@@ -154,6 +215,23 @@ export default {
});
},
}),
SetUserAlwaysPremodStatus: ({ variables: { status, id } }) => ({
updateQueries: {
TalkAdmin_Community: prev => {
if (!status) {
return prev;
}
const updated = update(prev, {
users: {
nodes: {
$apply: nodes => nodes.filter(node => node.id !== id),
},
},
});
return updated;
},
},
}),
UnbanUser: ({
variables: {
input: { id },
@@ -0,0 +1,29 @@
import {
SHOW_ALWAYS_PREMOD_USER_DIALOG,
HIDE_ALWAYS_PREMOD_USER_DIALOG,
} from '../constants/alwaysPremodUserDialog';
const initialState = {
open: false,
userId: null,
username: '',
};
export default function alwaysPremodUserDialog(state = initialState, action) {
switch (action.type) {
case SHOW_ALWAYS_PREMOD_USER_DIALOG:
return {
...state,
open: true,
userId: action.userId,
username: action.username,
};
case HIDE_ALWAYS_PREMOD_USER_DIALOG:
return {
...state,
open: false,
};
default:
return state;
}
}
+2
View File
@@ -5,6 +5,7 @@ import moderation from './moderation';
import install from './install';
import banUserDialog from './banUserDialog';
import suspendUserDialog from './suspendUserDialog';
import alwaysPremodUserDialog from './alwaysPremodUserDialog';
import rejectUsernameDialog from './rejectUsernameDialog';
import userDetail from './userDetail';
import ui from './ui';
@@ -14,6 +15,7 @@ export default {
banUserDialog,
configure,
suspendUserDialog,
alwaysPremodUserDialog,
userDetail,
stories,
community,
+14
View File
@@ -6,6 +6,10 @@ import {
SHOW_SUSPEND_USER_DIALOG,
HIDE_SUSPEND_USER_DIALOG,
} from '../constants/suspendUserDialog';
import {
SHOW_ALWAYS_PREMOD_USER_DIALOG,
HIDE_ALWAYS_PREMOD_USER_DIALOG,
} from '../constants/alwaysPremodUserDialog';
const initialState = {
modal: false,
@@ -23,6 +27,11 @@ export default function config(state = initialState, action) {
...state,
modal: true,
};
case SHOW_ALWAYS_PREMOD_USER_DIALOG:
return {
...state,
modal: true,
};
case HIDE_BAN_USER_DIALOG:
return {
...state,
@@ -33,6 +42,11 @@ export default function config(state = initialState, action) {
...state,
modal: false,
};
case HIDE_ALWAYS_PREMOD_USER_DIALOG:
return {
...state,
modal: false,
};
default:
return state;
}
@@ -130,6 +130,12 @@ th.header:nth-child(2), th.header:nth-child(3) {
font-size: 0.98em;
}
.actionsMenuAlwaysPremod {
background-color: #F0B50B;
border-color: #F0B50B;
color: white;
}
.actionsMenuSuspended {
background-color: #F29336;
border-color: #F29336;
@@ -8,7 +8,11 @@ import LoadMore from '../../../components/LoadMore';
import PropTypes from 'prop-types';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import { isSuspended, isBanned } from 'coral-framework/utils/user';
import {
isSuspended,
isBanned,
isAlwaysPremod,
} from 'coral-framework/utils/user';
import { RadioGroup, Radio } from 'react-mdl';
import moment from 'moment';
import {
@@ -43,6 +47,8 @@ class People extends React.Component {
return 'Banned';
} else if (isSuspended(user)) {
return 'Suspended';
} else if (isAlwaysPremod(user)) {
return 'Always premoderated';
}
return '';
};
@@ -55,6 +61,10 @@ class People extends React.Component {
this.props.unbanUser(input);
};
removeAlwaysPremodUser = input => {
this.props.removeAlwaysPremodUser(input);
};
showBanUserDialog = input => {
this.props.showBanUserDialog(input);
};
@@ -63,6 +73,10 @@ class People extends React.Component {
this.props.showSuspendUserDialog(input);
};
showAlwaysPremodUserDialog = input => {
this.props.showAlwaysPremodUserDialog(input);
};
render() {
const {
onFilterChange,
@@ -109,6 +123,7 @@ class People extends React.Component {
<Radio value="active">{t('community.active')}</Radio>
<Radio value="suspended">{t('community.suspended')}</Radio>
<Radio value="banned">{t('community.banned')}</Radio>
<Radio value="alwaysPremod">{t('community.always_premod')}</Radio>
</RadioGroup>
<div className={styles.filterDetail}>
{t('community.filter_role')}
@@ -188,6 +203,9 @@ class People extends React.Component {
user
),
[styles.actionsMenuBanned]: isBanned(user),
[styles.actionsMenuAlwaysPremod]: isAlwaysPremod(
user
),
},
'talk-admin-user-detail-actions-button'
)}
@@ -232,6 +250,27 @@ class People extends React.Component {
{t('modqueue.ban_user_actions')}
</ActionsMenuItem>
)}
{isAlwaysPremod(user) ? (
<ActionsMenuItem
onClick={() =>
this.removeAlwaysPremodUser({ id: user.id })
}
>
Remove Always Premoderate
</ActionsMenuItem>
) : (
<ActionsMenuItem
onClick={() =>
this.showAlwaysPremodUserDialog({
userId: user.id,
username: user.username,
})
}
>
{t('modqueue.always_premod_user_actions')}
</ActionsMenuItem>
)}
</ActionsMenu>
</td>
<td className="mdl-data-table__cell--non-numeric">
@@ -294,8 +333,10 @@ People.propTypes = {
viewUserDetail: PropTypes.func.isRequired,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
removeAlwaysPremodUser: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
showAlwaysPremodUserDialog: PropTypes.func,
loadMore: PropTypes.func.isRequired,
};
@@ -8,9 +8,11 @@ import {
withUnbanUser,
withUnsuspendUser,
withSetUserRole,
withRemoveAlwaysPremodUser,
} from 'coral-framework/graphql/mutations';
import { showBanUserDialog } from 'actions/banUserDialog';
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
import { showAlwaysPremodUserDialog } from 'actions/alwaysPremodUserDialog';
import { viewUserDetail } from '../../../actions/userDetail';
import { appendNewNodes } from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
@@ -30,6 +32,7 @@ class PeopleContainer extends React.PureComponent {
active: { suspended: false, banned: false },
suspended: { suspended: true },
banned: { banned: true },
alwaysPremod: { alwaysPremod: true },
};
onFilterChange = filter => e =>
@@ -40,7 +43,6 @@ class PeopleContainer extends React.PureComponent {
getFilterState = () => {
const { role, status } = this.state;
return {
status: this.statusToQuery[status] || null,
role: role || null,
@@ -106,8 +108,10 @@ class PeopleContainer extends React.PureComponent {
setUserRole={this.props.setUserRole}
showSuspendUserDialog={this.props.showSuspendUserDialog}
showBanUserDialog={this.props.showBanUserDialog}
showAlwaysPremodUserDialog={this.props.showAlwaysPremodUserDialog}
unbanUser={this.props.unbanUser}
unsuspendUser={this.props.unsuspendUser}
removeAlwaysPremodUser={this.props.removeAlwaysPremodUser}
data={this.props.data}
root={this.props.root}
users={this.props.root.users}
@@ -122,9 +126,11 @@ PeopleContainer.propTypes = {
setUserRole: PropTypes.func.isRequired,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
removeAlwaysPremodUser: PropTypes.func.isRequired,
viewUserDetail: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
showAlwaysPremodUserDialog: PropTypes.func,
data: PropTypes.object,
root: PropTypes.object,
};
@@ -153,6 +159,9 @@ const LOAD_MORE_QUERY = gql`
banned {
status
}
alwaysPremod {
status
}
suspension {
until
}
@@ -187,6 +196,9 @@ const FILTER_QUERY = gql`
banned {
status
}
alwaysPremod {
status
}
suspension {
until
}
@@ -203,6 +215,7 @@ const mapDispatchToProps = dispatch =>
viewUserDetail,
showSuspendUserDialog,
showBanUserDialog,
showAlwaysPremodUserDialog,
},
dispatch
);
@@ -215,6 +228,7 @@ export default compose(
withSetUserRole,
withUnsuspendUser,
withUnbanUser,
withRemoveAlwaysPremodUser,
withQuery(
gql`
query TalkAdmin_Community_People {
@@ -236,6 +250,9 @@ export default compose(
banned {
status
}
alwaysPremod {
status
}
suspension {
until
}
@@ -175,6 +175,9 @@ const USER_BANNED_SUBSCRIPTION = gql`
banned {
status
}
alwaysPremod {
status
}
suspension {
until
}
@@ -196,6 +199,9 @@ const USER_SUSPENDED_SUBSCRIPTION = gql`
banned {
status
}
alwaysPremod {
status
}
suspension {
until
}
@@ -217,6 +223,9 @@ const USERNAME_REJECTED_SUBSCRIPTION = gql`
banned {
status
}
alwaysPremod {
status
}
suspension {
until
}
@@ -253,6 +262,9 @@ const EMBED_QUERY = gql`
banned {
status
}
alwaysPremod {
status
}
suspension {
until
}
@@ -398,6 +398,9 @@ const fragments = {
banned {
status
}
alwaysPremod {
status
}
suspension {
until
}
@@ -3,6 +3,7 @@ import { createDefaultResponseFragments } from '../utils';
// fragments defined here are automatically registered.
export default {
...createDefaultResponseFragments(
'AlwaysPremodUserResponse',
'BanUsersResponse',
'ChangeUsernameResponse',
'CloseAssetResponse',
@@ -14,6 +15,7 @@ export default {
'IgnoreUserResponse',
'ModifyTagResponse',
'PostFlagResponse',
'RemoveAlwaysPremodUserResponse',
'SetCommentStatusResponse',
'SetUsernameResponse',
'SetUsernameStatusResponse',
@@ -412,6 +412,48 @@ export const withSetUsername = withMutation(
}
);
export const withAlwaysPremodUser = withMutation(
gql`
mutation AlwaysPremodUser($input: AlwaysPremodUserInput!) {
alwaysPremodUser(input: $input) {
...AlwaysPremodUserResponse
}
}
`,
{
props: ({ mutate }) => ({
alwaysPremodUser: input => {
return mutate({
variables: {
input,
},
});
},
}),
}
);
export const withRemoveAlwaysPremodUser = withMutation(
gql`
mutation RemoveAlwaysPremodUser($input: RemoveAlwaysPremodUserInput!) {
removeAlwaysPremodUser(input: $input) {
...RemoveAlwaysPremodUserResponse
}
}
`,
{
props: ({ mutate }) => ({
removeAlwaysPremodUser: input => {
return mutate({
variables: {
input,
},
});
},
}),
}
);
export const withBanUser = withMutation(
gql`
mutation BanUser($input: BanUserInput!) {
+9
View File
@@ -35,6 +35,15 @@ export const isBanned = user => {
return get(user, 'state.status.banned.status');
};
/**
* isAlwaysPremod
* retrieves boolean based on the user premod status
*/
export const isAlwaysPremod = user => {
return get(user, 'state.status.alwaysPremod.status');
};
/**
* isUsernameRejected
* retrieves boolean based on the username status
+7 -1
View File
@@ -11,7 +11,7 @@ const mergeState = (query, state) => {
}
if (status) {
const { username, banned, suspended } = status;
const { username, banned, suspended, alwaysPremod } = status;
if (typeof username !== 'undefined' && username && username.length > 0) {
query.merge({
@@ -27,6 +27,12 @@ const mergeState = (query, state) => {
});
}
if (typeof alwaysPremod !== 'undefined' && alwaysPremod !== null) {
query.merge({
'status.alwaysPremod.status': alwaysPremod,
});
}
if (typeof suspended !== 'undefined' && suspended !== null) {
if (suspended) {
query.merge({
+14
View File
@@ -10,6 +10,7 @@ const {
SET_USERNAME,
SET_USER_USERNAME_STATUS,
SET_USER_BAN_STATUS,
SET_USER_ALWAYS_PREMOD_STATUS,
SET_USER_SUSPENSION_STATUS,
UPDATE_USER_ROLES,
DELETE_OTHER_USER,
@@ -32,6 +33,13 @@ const setUserBanStatus = async (ctx, id, status = false, message = null) => {
}
};
const setUserAlwaysPremodStatus = async (ctx, id, status = false) => {
const user = await Users.setAlwaysPremodStatus(id, status, ctx.user.id);
if (user.alwaysPremod) {
ctx.pubsub.publish('userAlwaysPremod', user);
}
};
const setUserSuspensionStatus = async (
ctx,
id,
@@ -220,6 +228,7 @@ module.exports = ctx => {
setRole: () => Promise.reject(new ErrNotAuthorized()),
setUserBanStatus: () => Promise.reject(new ErrNotAuthorized()),
setUserSuspensionStatus: () => Promise.reject(new ErrNotAuthorized()),
setUserAlwaysPremodStatus: () => Promise.reject(new ErrNotAuthorized()),
setUserUsernameStatus: () => Promise.reject(new ErrNotAuthorized()),
setUsername: () => Promise.reject(new ErrNotAuthorized()),
stopIgnoringUser: () => Promise.reject(new ErrNotAuthorized()),
@@ -256,6 +265,11 @@ module.exports = ctx => {
setUserBanStatus(ctx, id, status, message);
}
if (ctx.user.can(SET_USER_ALWAYS_PREMOD_STATUS)) {
mutators.User.setUserAlwaysPremodStatus = (id, status) =>
setUserAlwaysPremodStatus(ctx, id, status);
}
if (ctx.user.can(SET_USER_SUSPENSION_STATUS)) {
mutators.User.setUserSuspensionStatus = (id, until, message) =>
setUserSuspensionStatus(ctx, id, until, message);
@@ -0,0 +1,7 @@
const { decorateUserField } = require('./util');
const AlwaysPremodStatusHistory = {};
decorateUserField(AlwaysPremodStatusHistory, 'assigned_by');
module.exports = AlwaysPremodStatusHistory;
+2
View File
@@ -3,6 +3,7 @@ const debug = require('debug')('talk:graph:resolvers');
const Action = require('./action');
const ActionSummary = require('./action_summary');
const AlwaysPremodStatusHistory = require('./always_premod_status_history');
const Asset = require('./asset');
const AssetActionSummary = require('./asset_action_summary');
const BannedStatusHistory = require('./banned_status_history');
@@ -37,6 +38,7 @@ const plugins = require('../../services/plugins');
let resolvers = {
Action,
ActionSummary,
AlwaysPremodStatusHistory,
Asset,
AssetActionSummary,
BannedStatusHistory,
+10
View File
@@ -74,6 +74,16 @@ const RootMutation = {
unbanUser: async (obj, { input: { id } }, { mutators: { User } }) => {
await User.setUserBanStatus(id, false);
},
alwaysPremodUser: async (obj, { input: { id } }, { mutators: { User } }) => {
await User.setUserAlwaysPremodStatus(id, true);
},
removeAlwaysPremodUser: async (
obj,
{ input: { id } },
{ mutators: { User } }
) => {
await User.setUserAlwaysPremodStatus(id, false);
},
ignoreUser: async (_, { id }, { mutators: { User } }) => {
await User.ignoreUser({ id });
},
+42
View File
@@ -142,6 +142,10 @@ input UserStatusInput {
# suspended will restrict the returned users to only those that are, or are not
# suspended. If not provided, no filtering will be performed.
suspended: Boolean
# alwaysPremod will restrict the returned users to only those that are, or are not
# marked as always premoderate. If not provided, no filtering will be performed.
alwaysPremod: Boolean
}
type UsernameStatusHistory {
@@ -166,6 +170,17 @@ type BannedStatus {
history: [BannedStatusHistory!]
}
type AlwaysPremodStatusHistory {
status: Boolean!
assigned_by: User
created_at: Date!
}
type AlwaysPremodStatus {
status: Boolean!
history: [AlwaysPremodStatusHistory!]
}
type SuspensionStatusHistory {
until: Date
assigned_by: User
@@ -184,6 +199,9 @@ type UserStatus {
# banned is the bool that determines if the user is banned or not.
banned: BannedStatus!
# alwaysPremod is the bool that determines if the user comments are always pushed to the premod queue or not
alwaysPremod: AlwaysPremodStatus!
# suspension is the date that the user is suspended until.
suspension: SuspensionStatus!
}
@@ -1446,6 +1464,22 @@ type UnbanUserResponse implements Response {
errors: [UserError!]
}
input AlwaysPremodUserInput {
id: ID!
}
input RemoveAlwaysPremodUserInput {
id: ID!
}
type AlwaysPremodUserResponse implements Response {
errors: [UserError!]
}
type RemoveAlwaysPremodUserResponse implements Response {
errors: [UserError!]
}
input SuspendUserInput {
id: ID!
message: String!
@@ -1545,6 +1579,14 @@ type RootMutation {
# Mutation is restricted.
unsuspendUser(input: UnsuspendUserInput!): UnsuspendUserResponse
# Sets the always premod status on a given user. Requires the `MODERATOR` role.
# Mutation is restricted.
alwaysPremodUser(input: AlwaysPremodUserInput!): AlwaysPremodUserResponse
# Sets the always premod status on a given user. Requires the `MODERATOR` role.
# Mutation is restricted.
removeAlwaysPremodUser(input: RemoveAlwaysPremodUserInput!): RemoveAlwaysPremodUserResponse
# Sets the ban status on a given user. Requires the `MODERATOR` role.
# Mutation is restricted.
banUser(input: BanUserInput!): BanUsersResponse
+14
View File
@@ -3,6 +3,13 @@ en:
sort_comments: 'Sort Comments'
view_options: 'View Options'
already_flagged_username: 'You have already flagged this username.'
alwayspremoddialog:
are_you_sure: 'Are you sure you would like to always premoderate {0}?'
always_premod_user: 'Always premoderate user?'
cancel: Cancel
note: 'Note: {0}'
note_always_premod_user: 'Always premoderating this user will place all of their comments in the Pre-Moderate queue.'
yes_always_premod_user: 'Yes, always premoderate user'
bandialog:
are_you_sure: 'Are you sure you would like to ban {0}?'
ban_user: 'Ban User?'
@@ -64,6 +71,7 @@ en:
admin: Administrator
ads_marketing: 'This looks like an ad/marketing'
all: 'All'
always_premod: 'Always premoderated'
are_you_sure: 'Are you sure you would like to ban {0}?'
ban_user: 'Ban User?'
banned: Banned
@@ -382,6 +390,7 @@ en:
actions: Actions
all: all
all_streams: 'All Streams'
always_premod_user_actions: 'Always Premod User'
approve: Approve
approved: Approved
ban_user: Ban
@@ -528,6 +537,8 @@ en:
username_flags: 'flags for this username'
user_detail:
all: All
always_premod: 'Always Premod User'
always_premoded: 'Always premoderated'
ban: 'Ban User'
banned: Banned
email: Email
@@ -539,6 +550,7 @@ en:
reject_rate: 'Reject Rate'
reject_username: 'Reject Username'
rejected: Rejected
remove_always_premod: 'Remove Always Premoderate'
remove_ban: 'Remove Ban'
remove_suspension: 'Remove Suspension'
suspend: 'Suspend User'
@@ -552,12 +564,14 @@ en:
username_rejected: 'Username rejected'
user_history:
action: Action
always_premod_removed: 'Always premoderate removed'
ban_removed: 'Ban removed'
date: Date
suspended: 'Suspended, {0}'
suspension_removed: 'Suspension removed'
system: System
taken_by: 'Taken By'
user_always_premoded: 'User always premoderated'
user_banned: 'User banned'
username_status: 'Username {0}'
user_impersonating: 'This user is impersonating'
+14
View File
@@ -3,6 +3,13 @@ es:
sort_comments: 'Ordenar comentarios'
view_options: 'Ver opciones'
already_flagged_username: 'Usted ya reportó a este usuario.'
alwayspremoddialog:
are_you_sure: '¿Estás seguro que quieres pre-moderar a {0}?'
always_premod_user: '¿Pre-moderar al Usuario?'
cancel: Cancelar
note: 'Nota: {0}'
note_always_premod_user: 'Pre-moderar a un usuario pondrá todos sus comentarios en la fila de Pre-Moderación.'
yes_always_premod_user: 'Sí, pre-moderar al usuario.'
bandialog:
are_you_sure: '¿Estás seguro que quieres suspender a {0}?'
ban_user: '¿Quieres suspender al Usuario?'
@@ -62,6 +69,7 @@ es:
admin: Administrator
ads_marketing: 'Esto parece ser un publicidad/acción de comercialización'
all: 'Todos'
always_premod: 'Pre-moderado'
are_you_sure: '¿Estás seguro que quieres suspender a {0}?'
ban_user: '¿Quieres suspender al Usuario?'
banned: Suspendido
@@ -355,6 +363,7 @@ es:
actions: Acciones
all: todos
all_streams: 'Todos los Hilos'
always_premod_user_actions: 'Siempre Pre-moderar Usuario'
approve: Aprobar
approved: Aprobado
ban_user: Bloquear
@@ -486,12 +495,15 @@ es:
username_flags: 'reportes para este nombre de usuario'
user_detail:
all: Todos
always_premod: 'Siempre pre-moderar usuario'
always_premoded: 'Siempre pre-moderado'
ban: 'Bloquear usuario'
banned: Baneado
email: 'Correo electrónico'
member_since: 'Miembro desde'
reject_rate: 'Promedio de rechazo'
rejected: Rechazado
remove_always_premod: 'Cancelar siempre pre-moderar'
remove_ban: 'Cancelar bloqueo'
remove_suspension: 'Cancelar suspensión'
suspend: 'Suspender usuario'
@@ -503,12 +515,14 @@ es:
username_rejected: 'Usuario rechazado'
user_history:
action: Acción
always_premod_removed: 'Siempre pre-moderar cancelado'
ban_removed: 'Bloqueo cancelado'
date: Fecha
suspended: 'Suspendido, {0}'
suspension_removed: 'Suspensión cancelada'
system: Sistema
taken_by: 'Está en uso'
user_always_premoded: 'Usuario siempre pre-moderado'
user_banned: 'Usuario bloqueado'
username_status: 'Nombre de usuario {0}'
user_impersonating: 'Este usuario suplanta a alguien'
+45
View File
@@ -180,6 +180,30 @@ const User = new Schema(
},
],
},
// alwaysPremod stores the current user premod status as well as the history of
// changes.
alwaysPremod: {
// Status stores the current user premod status.
status: {
type: Boolean,
required: true,
default: false,
index: true,
},
history: [
{
// Status stores the historical premod status.
status: Boolean,
// assigned_by stores the user id of the user who assigned this status.
assigned_by: { type: String, default: null },
// created_at stores the date when this status was assigned.
created_at: { type: Date, default: Date.now },
},
],
},
},
// IgnoresUsers is an array of user id's that the current user is ignoring.
@@ -359,6 +383,27 @@ User.virtual('banned')
});
});
/**
* alwaysPremod returns true when the user is currently in always premod, and sets the alwaysPremod
* status locally.
*/
User.virtual('alwaysPremod')
.get(function() {
return this.status.alwaysPremod.status;
})
.set(function(status) {
this.status.alwaysPremod.status = status;
if (!this.status.alwaysPremod.history) {
this.status.alwaysPremod.history = [];
}
this.status.alwaysPremod.history.push({
status,
created_at: new Date(),
});
});
/**
* suspended returns true when the user is currently suspended, and sets the
* suspension status locally.
+1
View File
@@ -7,6 +7,7 @@ module.exports = {
EDIT_COMMENT: 'EDIT_COMMENT',
SET_USER_USERNAME_STATUS: 'SET_USER_USERNAME_STATUS',
SET_USER_BAN_STATUS: 'SET_USER_BAN_STATUS',
SET_USER_ALWAYS_PREMOD_STATUS: 'SET_USER_ALWAYS_PREMOD_STATUS',
SET_USER_SUSPENSION_STATUS: 'SET_USER_SUSPENSION_STATUS',
SET_COMMENT_STATUS: 'SET_COMMENT_STATUS',
ADD_COMMENT_TAG: 'ADD_COMMENT_TAG',
+1
View File
@@ -47,6 +47,7 @@ module.exports = (user, perm) => {
case types.SET_USER_USERNAME_STATUS:
case types.SET_USER_BAN_STATUS:
case types.SET_USER_SUSPENSION_STATUS:
case types.SET_USER_ALWAYS_PREMOD_STATUS:
case types.UPDATE_ASSET_SETTINGS:
case types.UPDATE_ASSET_STATUS:
return check(user, ['ADMIN', 'MODERATOR']);
+3 -3
View File
@@ -9,9 +9,9 @@ module.exports = (
},
}
) => {
// If the settings say that we're in premod mode, then the comment is in
// premod status.
if (moderation === 'PRE') {
// If the settings say that we're in premod mode, or the user is flagged as
// always premod, then the comment is in premod status.
if (moderation === 'PRE' || ctx.user.status.alwaysPremod.status === true) {
return {
status: 'PREMOD',
};
+44
View File
@@ -207,6 +207,50 @@ class Users {
return user;
}
static async setAlwaysPremodStatus(id, status, assignedBy = null) {
let user = await User.findOneAndUpdate(
{
id,
'status.alwaysPremod.status': {
$ne: status,
},
},
{
$set: {
'status.alwaysPremod.status': status,
},
$push: {
'status.alwaysPremod.history': {
status,
assigned_by: assignedBy,
created_at: Date.now(),
},
},
},
{
new: true,
runValidators: true,
}
);
if (!user) {
user = await User.findOne({ id });
if (!user) {
throw new ErrNotFound();
}
if (user.status.alwaysPremod.status === status) {
return user;
}
throw new Error(
'always premod status change edit failed for an unknown reason'
);
}
return user;
}
static async setBanStatus(id, status, assignedBy = null, message) {
let user = await User.findOneAndUpdate(
{