From 8163b523015aa70db5c5b7c715a604e5e5dcfcfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20G=C3=A1lvez?= Date: Wed, 13 Feb 2019 13:08:22 -0600 Subject: [PATCH] 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 --- bin/cli-users | 3 +- .../src/actions/alwaysPremodUserDialog.js | 14 ++++ client/coral-admin/src/actions/community.js | 11 +++ .../src/components/AlwaysPremodUserDialog.css | 29 +++++++ .../src/components/AlwaysPremodUserDialog.js | 68 ++++++++++++++++ .../coral-admin/src/components/UserDetail.css | 6 ++ .../coral-admin/src/components/UserDetail.js | 30 +++++++ .../coral-admin/src/components/UserHistory.js | 4 + .../src/constants/alwaysPremodUserDialog.js | 2 + client/coral-admin/src/constants/community.js | 3 + .../src/containers/AlwaysPremodUserDialog.js | 66 ++++++++++++++++ client/coral-admin/src/containers/Layout.js | 2 + .../coral-admin/src/containers/UserDetail.js | 16 ++++ client/coral-admin/src/graphql/index.js | 78 +++++++++++++++++++ .../src/reducers/alwaysPremodUserDialog.js | 29 +++++++ client/coral-admin/src/reducers/index.js | 2 + client/coral-admin/src/reducers/ui.js | 14 ++++ .../routes/Community/components/People.css | 6 ++ .../src/routes/Community/components/People.js | 43 +++++++++- .../src/routes/Community/containers/People.js | 19 ++++- .../src/containers/Embed.js | 12 +++ .../src/tabs/stream/containers/Stream.js | 3 + client/coral-framework/graphql/fragments.js | 2 + client/coral-framework/graphql/mutations.js | 42 ++++++++++ client/coral-framework/utils/user.js | 9 +++ graph/loaders/users.js | 8 +- graph/mutators/user.js | 14 ++++ .../resolvers/always_premod_status_history.js | 7 ++ graph/resolvers/index.js | 2 + graph/resolvers/root_mutation.js | 10 +++ graph/typeDefs.graphql | 42 ++++++++++ locales/en.yml | 14 ++++ locales/es.yml | 14 ++++ models/schema/user.js | 45 +++++++++++ perms/constants/mutation.js | 1 + perms/reducers/mutation.js | 1 + services/moderation/phases/premod.js | 6 +- services/users.js | 44 +++++++++++ 38 files changed, 714 insertions(+), 7 deletions(-) create mode 100644 client/coral-admin/src/actions/alwaysPremodUserDialog.js create mode 100644 client/coral-admin/src/components/AlwaysPremodUserDialog.css create mode 100644 client/coral-admin/src/components/AlwaysPremodUserDialog.js create mode 100644 client/coral-admin/src/constants/alwaysPremodUserDialog.js create mode 100644 client/coral-admin/src/containers/AlwaysPremodUserDialog.js create mode 100644 client/coral-admin/src/reducers/alwaysPremodUserDialog.js create mode 100644 graph/resolvers/always_premod_status_history.js diff --git a/bin/cli-users b/bin/cli-users index 743119a0a..3e8bc7741 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -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()); diff --git a/client/coral-admin/src/actions/alwaysPremodUserDialog.js b/client/coral-admin/src/actions/alwaysPremodUserDialog.js new file mode 100644 index 000000000..5c5cfe7b8 --- /dev/null +++ b/client/coral-admin/src/actions/alwaysPremodUserDialog.js @@ -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, +}); diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index c7c7cf2a3..f3bd6301e 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -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, diff --git a/client/coral-admin/src/components/AlwaysPremodUserDialog.css b/client/coral-admin/src/components/AlwaysPremodUserDialog.css new file mode 100644 index 000000000..5bf1beeb7 --- /dev/null +++ b/client/coral-admin/src/components/AlwaysPremodUserDialog.css @@ -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; +} diff --git a/client/coral-admin/src/components/AlwaysPremodUserDialog.js b/client/coral-admin/src/components/AlwaysPremodUserDialog.js new file mode 100644 index 000000000..7d01d6259 --- /dev/null +++ b/client/coral-admin/src/components/AlwaysPremodUserDialog.js @@ -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 ( + + + × + +
+

+ {t('alwayspremoddialog.always_premod_user')} +

+

+ {t('alwayspremoddialog.are_you_sure', username)} +

+

{info}

+
+ + +
+
+
+ ); + } +} + +AlwaysPremodUserDialog.propTypes = { + open: PropTypes.bool, + onPerform: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + username: PropTypes.string, + info: PropTypes.string, +}; + +export default AlwaysPremodUserDialog; diff --git a/client/coral-admin/src/components/UserDetail.css b/client/coral-admin/src/components/UserDetail.css index c35e4776f..eb379c00f 100644 --- a/client/coral-admin/src/components/UserDetail.css +++ b/client/coral-admin/src/components/UserDetail.css @@ -137,6 +137,12 @@ display: inline-block; } +.actionsMenuAlwaysPremod { + background-color: #F0B50B; + border-color: #F0B50B; + color: white; +} + .actionsMenuSuspended { background-color: #F29336; border-color: #F29336; diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index 9bde04f96..f6806915d 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -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 ( @@ -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 { )} + {alwaysPremod ? ( + removeAlwaysPremodUser({ id: user.id })} + > + {t('user_detail.remove_always_premod')} + + ) : ( + + {t('user_detail.always_premod')} + + )} + {usernameChanged && ( {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, }; diff --git a/client/coral-admin/src/components/UserHistory.js b/client/coral-admin/src/components/UserHistory.js index 3a3472c86..fb797c1cd 100644 --- a/client/coral-admin/src/components/UserHistory.js +++ b/client/coral-admin/src/components/UserHistory.js @@ -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)) diff --git a/client/coral-admin/src/constants/alwaysPremodUserDialog.js b/client/coral-admin/src/constants/alwaysPremodUserDialog.js new file mode 100644 index 000000000..0728bbc43 --- /dev/null +++ b/client/coral-admin/src/constants/alwaysPremodUserDialog.js @@ -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'; diff --git a/client/coral-admin/src/constants/community.js b/client/coral-admin/src/constants/community.js index 1ffe3f8d6..b47ec0455 100644 --- a/client/coral-admin/src/constants/community.js +++ b/client/coral-admin/src/constants/community.js @@ -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`; diff --git a/client/coral-admin/src/containers/AlwaysPremodUserDialog.js b/client/coral-admin/src/containers/AlwaysPremodUserDialog.js new file mode 100644 index 000000000..e6e195810 --- /dev/null +++ b/client/coral-admin/src/containers/AlwaysPremodUserDialog.js @@ -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 ( + + ); + } +} + +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); diff --git a/client/coral-admin/src/containers/Layout.js b/client/coral-admin/src/containers/Layout.js index 22c624b7d..10e4666f8 100644 --- a/client/coral-admin/src/containers/Layout.js +++ b/client/coral-admin/src/containers/Layout.js @@ -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} > + diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index e9ffea4d1..7053f6c0b 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -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 diff --git a/client/coral-admin/src/graphql/index.js b/client/coral-admin/src/graphql/index.js index 9fca9f497..11eea3d18 100644 --- a/client/coral-admin/src/graphql/index.js +++ b/client/coral-admin/src/graphql/index.js @@ -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 }, diff --git a/client/coral-admin/src/reducers/alwaysPremodUserDialog.js b/client/coral-admin/src/reducers/alwaysPremodUserDialog.js new file mode 100644 index 000000000..a5da4c54b --- /dev/null +++ b/client/coral-admin/src/reducers/alwaysPremodUserDialog.js @@ -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; + } +} diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js index e806f5d11..cded51943 100644 --- a/client/coral-admin/src/reducers/index.js +++ b/client/coral-admin/src/reducers/index.js @@ -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, diff --git a/client/coral-admin/src/reducers/ui.js b/client/coral-admin/src/reducers/ui.js index dcb9ddbe0..263ae4ddd 100644 --- a/client/coral-admin/src/reducers/ui.js +++ b/client/coral-admin/src/reducers/ui.js @@ -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; } diff --git a/client/coral-admin/src/routes/Community/components/People.css b/client/coral-admin/src/routes/Community/components/People.css index aea321cd7..4b7cfd4a8 100644 --- a/client/coral-admin/src/routes/Community/components/People.css +++ b/client/coral-admin/src/routes/Community/components/People.css @@ -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; diff --git a/client/coral-admin/src/routes/Community/components/People.js b/client/coral-admin/src/routes/Community/components/People.js index e40b6cca4..cbcb74bc9 100644 --- a/client/coral-admin/src/routes/Community/components/People.js +++ b/client/coral-admin/src/routes/Community/components/People.js @@ -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 { {t('community.active')} {t('community.suspended')} {t('community.banned')} + {t('community.always_premod')}
{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')} )} + + {isAlwaysPremod(user) ? ( + + this.removeAlwaysPremodUser({ id: user.id }) + } + > + Remove Always Premoderate + + ) : ( + + this.showAlwaysPremodUserDialog({ + userId: user.id, + username: user.username, + }) + } + > + {t('modqueue.always_premod_user_actions')} + + )} @@ -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, }; diff --git a/client/coral-admin/src/routes/Community/containers/People.js b/client/coral-admin/src/routes/Community/containers/People.js index 7f47795b4..57fb7fa8e 100644 --- a/client/coral-admin/src/routes/Community/containers/People.js +++ b/client/coral-admin/src/routes/Community/containers/People.js @@ -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 } diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index 836def259..ef7c361a5 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -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 } diff --git a/client/coral-embed-stream/src/tabs/stream/containers/Stream.js b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js index 1f32fcedf..a379e6dbd 100644 --- a/client/coral-embed-stream/src/tabs/stream/containers/Stream.js +++ b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js @@ -398,6 +398,9 @@ const fragments = { banned { status } + alwaysPremod { + status + } suspension { until } diff --git a/client/coral-framework/graphql/fragments.js b/client/coral-framework/graphql/fragments.js index 778214b8b..976776933 100644 --- a/client/coral-framework/graphql/fragments.js +++ b/client/coral-framework/graphql/fragments.js @@ -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', diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js index 8191c2cd0..78c044270 100644 --- a/client/coral-framework/graphql/mutations.js +++ b/client/coral-framework/graphql/mutations.js @@ -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!) { diff --git a/client/coral-framework/utils/user.js b/client/coral-framework/utils/user.js index cdc0884d5..174815bfb 100644 --- a/client/coral-framework/utils/user.js +++ b/client/coral-framework/utils/user.js @@ -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 diff --git a/graph/loaders/users.js b/graph/loaders/users.js index 11d35ac95..cc91af340 100644 --- a/graph/loaders/users.js +++ b/graph/loaders/users.js @@ -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({ diff --git a/graph/mutators/user.js b/graph/mutators/user.js index fa6a18ee5..3732ee0de 100644 --- a/graph/mutators/user.js +++ b/graph/mutators/user.js @@ -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); diff --git a/graph/resolvers/always_premod_status_history.js b/graph/resolvers/always_premod_status_history.js new file mode 100644 index 000000000..9fccae1d7 --- /dev/null +++ b/graph/resolvers/always_premod_status_history.js @@ -0,0 +1,7 @@ +const { decorateUserField } = require('./util'); + +const AlwaysPremodStatusHistory = {}; + +decorateUserField(AlwaysPremodStatusHistory, 'assigned_by'); + +module.exports = AlwaysPremodStatusHistory; diff --git a/graph/resolvers/index.js b/graph/resolvers/index.js index 4bc9d9624..7dc9d24c8 100644 --- a/graph/resolvers/index.js +++ b/graph/resolvers/index.js @@ -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, diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js index d2c2965e0..6572cdca2 100644 --- a/graph/resolvers/root_mutation.js +++ b/graph/resolvers/root_mutation.js @@ -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 }); }, diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index ebbf8203d..d1f2ca5a5 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -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 diff --git a/locales/en.yml b/locales/en.yml index ad831f5a3..dc5653ed1 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -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' diff --git a/locales/es.yml b/locales/es.yml index cff505cbd..57a789510 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -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' diff --git a/models/schema/user.js b/models/schema/user.js index 40dcd5ec8..d4dca3a1c 100644 --- a/models/schema/user.js +++ b/models/schema/user.js @@ -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. diff --git a/perms/constants/mutation.js b/perms/constants/mutation.js index 2d4ae04cb..54b92e00b 100644 --- a/perms/constants/mutation.js +++ b/perms/constants/mutation.js @@ -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', diff --git a/perms/reducers/mutation.js b/perms/reducers/mutation.js index 8133fcd47..93cb0ab24 100644 --- a/perms/reducers/mutation.js +++ b/perms/reducers/mutation.js @@ -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']); diff --git a/services/moderation/phases/premod.js b/services/moderation/phases/premod.js index db1a3fd1b..36aba634e 100644 --- a/services/moderation/phases/premod.js +++ b/services/moderation/phases/premod.js @@ -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', }; diff --git a/services/users.js b/services/users.js index 2ce0b7c15..e85ed67a2 100644 --- a/services/users.js +++ b/services/users.js @@ -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( {