Merge branch 'next' into user-status-refactor

This commit is contained in:
Wyatt Johnson
2017-11-08 13:52:48 -07:00
35 changed files with 541 additions and 147 deletions
@@ -7,11 +7,11 @@ import {Icon} from 'coral-ui';
import t from 'coral-framework/services/i18n';
const ApproveButton = ({active, minimal, onClick}) => {
const ApproveButton = ({active, minimal, onClick, className}) => {
const text = active ? t('modqueue.approved') : t('modqueue.approve');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active})}
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
onClick={onClick}
>
<Icon name={'done'} className={styles.icon} />
@@ -21,6 +21,7 @@ const ApproveButton = ({active, minimal, onClick}) => {
};
ApproveButton.propTypes = {
className: PropTypes.string,
active: PropTypes.bool,
minimal: PropTypes.bool,
onClick: PropTypes.func,
@@ -15,7 +15,15 @@ function generateRegExp(phrases) {
.join('[\\s"?!.]+')
).join('|');
return new RegExp(`(^|[^\\w])(${inner})(?=[^\\w]|$)`, 'iu');
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
try {
return new RegExp(pattern, 'iu');
}
catch (_err) {
// IE does not support unicode support, so we'll create one without.
return new RegExp(pattern, 'i');
}
}
// Generate a regular expression detecting `suspectWords` and `bannedWords` phrases.
@@ -7,11 +7,11 @@ import {Icon} from 'coral-ui';
import t from 'coral-framework/services/i18n';
const RejectButton = ({active, minimal, onClick}) => {
const RejectButton = ({active, minimal, onClick, className}) => {
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active})}
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
onClick={onClick}
>
<Icon name={'close'} className={styles.icon} />
@@ -21,6 +21,7 @@ const RejectButton = ({active, minimal, onClick}) => {
};
RejectButton.propTypes = {
className: PropTypes.string,
active: PropTypes.bool,
minimal: PropTypes.bool,
onClick: PropTypes.func,
@@ -76,9 +76,9 @@ const CoralHeader = ({
}
<div className={styles.rightPanel}>
<ul>
<li className={styles.settings}>
<li className={cn(styles.settings, 'talk-admin-header-settings')}>
<div>
<IconButton name="settings" id="menu-settings"/>
<IconButton name="settings" id="menu-settings" className="talk-admin-header-settings-button"/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
<MenuItem>
@@ -91,7 +91,7 @@ const CoralHeader = ({
Report a bug or give feedback
</a>
</MenuItem>
<MenuItem onClick={handleLogout}>
<MenuItem onClick={handleLogout} className="talk-admin-header-sign-out">
{t('configure.sign_out')}
</MenuItem>
</Menu>
@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
import EmptyCard from 'coral-admin/src/components/EmptyCard';
import LoadMore from '../../../components/LoadMore';
@@ -23,7 +24,7 @@ class FlaggedAccounts extends React.Component {
const hasResults = users.nodes && !!users.nodes.length;
return (
<div className={styles.container}>
<div className={cn('talk-adnin-community-flagged-accounts', styles.container)}>
<div className={styles.mainFlaggedContent}>
{
hasResults
@@ -70,4 +71,15 @@ class FlaggedAccounts extends React.Component {
}
}
FlaggedAccounts.propTypes = {
users: PropTypes.object,
loadMore: PropTypes.func,
showBanUserDialog: PropTypes.func,
showSuspendUserDialog: PropTypes.func,
showRejectUsernameDialog: PropTypes.func,
approveUser: PropTypes.func,
me: PropTypes.object,
viewUserDetail: PropTypes.func,
};
export default FlaggedAccounts;
@@ -1,18 +1,14 @@
import React from 'react';
import styles from './FlaggedUser.css';
// TODO: Should not rely on plugin.
import PropTypes from 'prop-types';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
import {username} from 'talk-plugin-flags/helpers/flagReasons';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import ApproveButton from 'coral-admin/src/components/ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
// TODO: Should work with custom flags too.
const shortReasons = {
[username.other]: t('community.other'),
[username.spam]: t('community.spam_ads'),
@@ -21,7 +17,6 @@ const shortReasons = {
[username.impersonating]: t('community.impersonating'),
};
// Render a single user for the list
class User extends React.Component {
showSuspenUserDialog = () => this.props.showSuspendUserDialog({
@@ -50,12 +45,10 @@ class User extends React.Component {
} = this.props;
return (
<li
tabIndex={0}
className={cn(className, styles.root, {[styles.rootSelected]: selected})}
>
<div className={styles.container}>
<div className={styles.header}>
<li tabIndex={0}
className={cn(className, styles.root, {[styles.rootSelected]: selected})} >
<div className={cn('talk-admin-community-flagged-user', styles.container)}>
<div className={cn('talk-admin-community-flagged-user-header', styles.header)}>
<div className={styles.author}>
<button
onClick={this.viewAuthorDetail}
@@ -78,8 +71,7 @@ class User extends React.Component {
}
</div>
</div>
<div className={styles.body}>
<div className={cn('talk-admin-community-flagged-user-body', styles.body)}>
<div className={styles.flagged}>
<div className={styles.flaggedByCount}>
<i className={cn('material-icons', styles.flagIcon)}>flag</i>
@@ -127,9 +119,11 @@ class User extends React.Component {
<div className={styles.sideActions}>
<div className={styles.actions}>
<ApproveButton
className="talk-admin-flagged-user-approve-button"
onClick={this.approveUser}
/>
<RejectButton
className="talk-admin-flagged-user-reject-button"
onClick={this.showRejectUsernameDialog}
/>
</div>
@@ -141,4 +135,16 @@ class User extends React.Component {
}
}
User.propTypes = {
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
viewUserDetail: PropTypes.func,
showRejectUsernameDialog: PropTypes.func,
approveUser: PropTypes.func,
user: PropTypes.object,
className: PropTypes.string,
selected: PropTypes.bool,
me: PropTypes.object,
};
export default User;
@@ -1,6 +1,6 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import {Dialog, Button} from 'coral-ui';
import styles from './RejectUsernameDialog.css';
@@ -29,12 +29,6 @@ class RejectUsernameDialog extends Component {
state = {email: '', stage: 0}
static propTypes = {
stage: PropTypes.number,
handleClose: PropTypes.func.isRequired,
rejectUsername: PropTypes.func.isRequired
}
componentDidMount() {
this.setState({email: t('reject_username.email_message_reject'), about: t('reject_username.username')});
}
@@ -76,7 +70,7 @@ class RejectUsernameDialog extends Component {
const {stage} = this.state;
return <Dialog
className={styles.suspendDialog}
className={cn(styles.suspendDialog, 'talk-reject-username-dialog')}
id="rejectUsernameDialog"
open={open}
onClose={handleClose}
@@ -96,15 +90,18 @@ class RejectUsernameDialog extends Component {
<div className={styles.emailContainer}>
<textarea
rows={5}
className={styles.emailInput}
className={cn(styles.emailInput, 'talk-reject-username-dialog-suspension-message')}
value={this.state.email}
onChange={this.onEmailChange}/>
</div>
</div>
}
<div className={styles.modalButtons}>
<div className={cn(styles.modalButtons, 'talk-reject-username-dialog-buttons')}>
{Object.keys(stages[stage].options).map((key, i) => (
<Button key={i} onClick={this.onActionClick(stage, i)}>
<Button
key={i}
className={cn('talk-reject-username-dialog-button', `talk-reject-username-dialog-button-${key}`)}
onClick={this.onActionClick(stage, i)} >
{t(stages[stage].options[key], t('reject_username.username'))}
</Button>
))}
@@ -114,4 +111,12 @@ class RejectUsernameDialog extends Component {
}
}
RejectUsernameDialog.propTypes = {
stage: PropTypes.number,
handleClose: PropTypes.func.isRequired,
rejectUsername: PropTypes.func.isRequired,
user: PropTypes.object,
open: PropTypes.bool,
};
export default RejectUsernameDialog;
@@ -213,12 +213,14 @@ class Stream extends React.Component {
const open = !asset.isClosed;
const banned = user && user.status === 'BANNED';
const pending = user && user.status === 'PENDING';
const temporarilySuspended =
user &&
user.suspension.until &&
new Date(user.suspension.until) > new Date();
const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !highlightedComment) || keepCommentBox);
const showCommentBox = loggedIn && ((!banned && !pending & !temporarilySuspended && !highlightedComment) || keepCommentBox);
const slotProps = {data};
const slotQueryData = {root, asset};
@@ -1,6 +1,10 @@
.editNameInput {
margin-top: 10px;
margin-bottom: 10px;
padding: 10px;
border-radius: 3px;
border: solid 1px #d8d8d8;
font-size: 0.9em;
}
.alert {
@@ -1,5 +1,6 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
import styles from './SuspendAccount.css';
import {Button} from 'coral-ui';
@@ -66,17 +67,16 @@ class SuspendedAccount extends Component {
</label>
<input
type='text'
className={styles.editNameInput}
className={cn(styles.editNameInput, 'talk-suspended-account-username-input')}
value={username}
placeholder={t('framework.edit_name.label')}
id='username'
onChange={(e) => this.setState({username: e.target.value})}
rows={3}/><br/>
<Button
onClick={this.onSubmitClick}>
{
t('framework.edit_name.button')
}
className="talk-suspended-account-submit-button"
onClick={this.onSubmitClick} >
{t('framework.edit_name.button')}
</Button>
</div> : null
}
@@ -1,16 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
import RestrictedMessageBox from './RestrictedMessageBox';
export default ({children, restricted, message = t('framework.content_not_available'), restrictedComp}) => {
const RestrictedContent = ({children, restricted, message = t('framework.content_not_available'), restrictedComp}) => {
if (restricted) {
return restrictedComp ? restrictedComp : <RestrictedMessageBox message={message} />;
} else {
return (
<div>
<div className="talk-restricted-content">
{children}
</div>
);
}
};
RestrictedContent.propTypes = {
children: PropTypes.node,
restricted: PropTypes.bool,
message: PropTypes.string,
restrictedComp: PropTypes.node,
};
export default RestrictedContent;
@@ -1,4 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './RestrictedMessageBox.css';
export default ({children}) => <div className={styles.message}>{children}</div>;
const RestrictedMessageBox = ({children}) =>
<div className={cn(styles.message, 'talk-restricted-message-box')}>{children}</div>;
RestrictedMessageBox.propTypes = {
children: PropTypes.node,
};
export default RestrictedMessageBox;
@@ -7,6 +7,7 @@ import {PopupMenu, Button} from 'coral-ui';
import ClickOutside from 'coral-framework/components/ClickOutside';
import cn from 'classnames';
import styles from './styles.css';
import * as REASONS from '../helpers/flagReasons';
import {getErrorMessages} from 'coral-framework/utils';
@@ -90,10 +91,9 @@ export default class FlagButton extends Component {
let action = {
item_id,
item_type: itemType,
reason: null,
message
};
if (reason === 'COMMENT_NOAGREE') {
if (reason === REASONS.comment.noagree) {
postDontAgree(action)
.then(({data}) => {
if (itemType === 'COMMENTS') {
@@ -122,7 +122,7 @@ export default class FlagButton extends Component {
onPopupOptionClick = (sets) => (e) => {
// If flagging a user, indicate that this is referencing the username rather than the bio
if(sets === 'itemType' && e.target.value === 'users') {
if (sets === 'itemType' && e.target.value === 'users') {
this.setState({field: 'username'});
}
+16 -2
View File
@@ -1,12 +1,26 @@
const DataLoader = require('dataloader');
const TagsService = require('../../services/tags');
const plugins = require('../../services/plugins');
const debug = require('debug')('talk:graph:loaders:tags');
const PLUGIN_TAGS = plugins.get('server', 'tags').reduce((acc, {plugin, tags}) => {
debug(`added plugin '${plugin.name}'`);
acc = acc.concat(tags);
return acc;
}, []);
/**
* Get all the tags for the context for the dataloader.
*/
const genAll = (context, queries) => {
return Promise.all(queries.map(({id, item_type, asset_id}) => {
return TagsService.getAll({id, item_type, asset_id});
return Promise.all(queries.map(async ({id, item_type, asset_id}) => {
let tags = await TagsService.getAll({id, item_type, asset_id});
// Merge in the global plugin tags as well.
tags = tags.concat(PLUGIN_TAGS);
return tags;
}));
};
+56 -29
View File
@@ -2,25 +2,47 @@ const ActionsService = require('../../services/actions');
const errors = require('../../errors');
const {CREATE_ACTION, DELETE_ACTION} = require('../../perms/constants');
/**
* getActionItem will return the item that is associated with the given action.
* If it does not exist, it will throw an error.
*
* @param {Object} ctx the graphql context for the request
* @param {Object} action the action being performed
* @return {Promise} resolves to the referenced item
*/
const getActionItem = async ({loaders: {Comments, Users}}, {item_id, item_type}) => {
if (item_type === 'COMMENTS') {
const comment = await Comments.get.load(item_id);
if (!comment) {
throw errors.ErrNotFound;
}
return comment;
} else if (item_type === 'USERS') {
const user = await Users.getByID.load(item_id);
if (!user) {
throw errors.ErrNotFound;
}
return user;
}
};
/**
* Creates an action on a item. If the item is a user flag, sets the user's status to
* pending.
* @param {Object} user the user performing the request
* @param {String} item_id id of the item to add the action to
* @param {String} item_type type of the item
* @param {String} action_type type of the action
* @return {Promise} resolves to the action created
*
* @param {Object} ctx the graphql context for the request
* @param {Object} action the action being created
* @return {Promise} resolves to the action created
*/
const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id, item_type, action_type, group_id, metadata = {}}) => {
const createAction = async (ctx, {item_id, item_type, action_type, group_id, metadata = {}}) => {
const {user = {}, pubsub} = ctx;
let comment;
if (item_type === 'COMMENTS') {
comment = await Comments.get.load(item_id);
if (!comment) {
throw new Error('Comment not found');
}
}
// Gets the item referenced by the action.
const item = await getActionItem(ctx, {item_id, item_type});
// Create the action itself.
let action = await ActionsService.create({
item_id,
item_type,
@@ -30,8 +52,13 @@ const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id,
metadata
});
if (comment) {
pubsub.publish('commentFlagged', comment);
// If the action is a flag.
if (action_type === 'FLAG') {
if (item_type === 'COMMENTS') {
// Push that the comment was flagged, don't wait for it to finish.
pubsub.publish('commentFlagged', item);
}
}
return action;
@@ -39,28 +66,28 @@ const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id,
/**
* Deletes an action based on the user id if the user owns that action.
*
* @param {Object} user the user performing the request
* @param {String} id the id of the action to delete
* @return {Promise} resolves to the deleted action, or null if not found.
*/
const deleteAction = ({user}, {id}) => {
return ActionsService.delete({id, user_id: user.id});
};
const deleteAction = ({user}, {id}) => ActionsService.delete({id, user_id: user.id});
module.exports = (context) => {
if (context.user && context.user.can(CREATE_ACTION, DELETE_ACTION)) {
return {
Action: {
create: (action) => createAction(context, action),
delete: (action) => deleteAction(context, action)
}
};
}
return {
module.exports = (ctx) => {
const mutators = {
Action: {
create: () => Promise.reject(errors.ErrNotAuthorized),
delete: () => Promise.reject(errors.ErrNotAuthorized)
}
};
if (ctx.user && ctx.user.can(CREATE_ACTION)) {
mutators.Action.create = (action) => createAction(ctx, action);
}
if (ctx.user && ctx.user.can(DELETE_ACTION)) {
mutators.Action.delete = (action) => deleteAction(ctx, action);
}
return mutators;
};
+7 -25
View File
@@ -1,15 +1,12 @@
const errors = require('../../errors');
const ActionModel = require('../../models/action');
const AssetsService = require('../../services/assets');
const ActionsService = require('../../services/actions');
const TagsService = require('../../services/tags');
const CommentsService = require('../../services/comments');
const KarmaService = require('../../services/karma');
const tlds = require('tlds');
const merge = require('lodash/merge');
const linkify = require('linkify-it')()
.tlds(tlds);
const linkify = require('linkify-it')().tlds(require('tlds'));
const Wordlist = require('../../services/wordlist');
const {
CREATE_COMMENT,
@@ -17,21 +14,8 @@ const {
ADD_COMMENT_TAG,
EDIT_COMMENT
} = require('../../perms/constants');
const {
DISABLE_AUTOFLAG_SUSPECT_WORDS
} = require('../../config');
const debug = require('debug')('talk:graph:mutators:tags');
const plugins = require('../../services/plugins');
const pluginTags = plugins.get('server', 'tags').reduce((acc, {plugin, tags}) => {
debug(`added plugin '${plugin.name}'`);
acc = acc.concat(tags);
return acc;
}, []);
const debug = require('debug')('talk:graph:mutators:comment');
const {DISABLE_AUTOFLAG_SUSPECT_WORDS} = require('../../config');
const resolveTagsForComment = async ({user, loaders: {Tags}}, {asset_id, tags = []}) => {
const item_type = 'COMMENTS';
@@ -48,8 +32,6 @@ const resolveTagsForComment = async ({user, loaders: {Tags}}, {asset_id, tags =
globalTags = [];
}
globalTags = globalTags.concat(pluginTags);
// Merge in the tags for the given comment.
tags = tags.map((name) => {
@@ -285,14 +267,14 @@ const moderationPhases = [
actions: [{
action_type: 'FLAG',
user_id: null,
group_id: 'Matched suspect word filter',
group_id: 'SUSPECT_WORD',
metadata: {}
}],
};
}
},
// This phase checks to see if the comment's length exeeds maximum.
// This phase checks to see if the comment's length exceeds maximum.
(context, comment, {assetSettings: {charCountEnable, charCount}}) => {
// Reject if the comment is too long
@@ -320,7 +302,7 @@ const moderationPhases = [
// Add the flag related to Trust to the comment.
return {
status:'SYSTEM_WITHHELD',
status: 'SYSTEM_WITHHELD',
actions: [{
action_type: 'FLAG',
user_id: null,
@@ -362,7 +344,7 @@ const moderationPhases = [
}
},
// This phase checks to see if the comment was already perscribed a status.
// This phase checks to see if the comment was already prescribed a status.
(context, comment) => {
// If the status was already defined, don't redefine it. It's only defined
+1 -1
View File
@@ -11,7 +11,7 @@ const modify = async ({user, loaders: {Tags}}, operation, {name, id, item_type,
const tags = await Tags.getAll.load({id, item_type, asset_id});
// Resolve the TagLink that should be used to insert to the user. This will
// addtionally return with an ownership property that can be used to determine
// additionally return with an ownership property that can be used to determine
// that the user who adds this tag must also be the owner of the resource.
let {tagLink, ownership} = TagsService.resolveLink(user, tags, {name, item_type});
+1 -7
View File
@@ -1,9 +1,3 @@
const DontAgreeAction = {
// Stored in the metadata, extract and return.
reason({metadata: {reason}}) {
return reason;
}
};
const DontAgreeAction = {};
module.exports = DontAgreeAction;
+1 -5
View File
@@ -1,7 +1,3 @@
const DontAgreeActionSummary = {
reason({group_id}) {
return group_id;
}
};
const DontAgreeActionSummary = {};
module.exports = DontAgreeActionSummary;
+2 -2
View File
@@ -13,8 +13,8 @@ const RootMutation = {
createFlag: async (_, {flag: {item_id, item_type, reason, message}}, {mutators: {Action}}) => ({
flag: Action.create({item_id, item_type, action_type: 'FLAG', group_id: reason, metadata: {message}}),
}),
createDontAgree: async (_, {dontagree: {item_id, item_type, reason, message}}, {mutators: {Action}}) => ({
dontagree: await Action.create({item_id, item_type, action_type: 'DONTAGREE', group_id: reason, metadata: {message}}),
createDontAgree: async (_, {dontagree: {item_id, item_type, message}}, {mutators: {Action}}) => ({
dontagree: await Action.create({item_id, item_type, action_type: 'DONTAGREE', metadata: {message}}),
}),
deleteAction: async (_, {id}, {mutators: {Action}}) => {
await Action.delete({id});
+31 -10
View File
@@ -601,6 +601,36 @@ type FlagAssetActionSummary implements AssetActionSummary {
actionableItemCount: Int
}
enum FLAG_REASON {
# The current user thinks that the flagged username is offensive.
USERNAME_OFFENSIVE
# The current user does not like the flagged username.
USERNAME_NOLIKE
# The current user thinks that the flagged username is being used to
# impersonate another user.
USERNAME_IMPERSONATING
# The current user thinks that the flagged username is spam.
USERNAME_SPAM
# The current user thinks that the flagged username is wrong for another
# reason.
USERNAME_OTHER
# The current user thinks that the flagged comment is offensive.
COMMENT_OFFENSIVE
# The current user thinks that the flagged comment is spam.
COMMENT_SPAM
# The current user thinks that the flagged comment is wrong for another
# reason.
COMMENT_OTHER
}
# A FLAG action that contains flag metadata.
type FlagAction implements Action {
@@ -629,9 +659,6 @@ type DontAgreeAction implements Action {
# The ID of the DontAgree Action.
id: ID!
# The reason for which the DontAgree Action was created.
reason: String
# An optional message sent with the flagging action by the user.
message: String
@@ -664,9 +691,6 @@ type DontAgreeActionSummary implements ActionSummary {
# The total count of flags with this reason.
count: Int!
# The reason for which the Flag Action was created.
reason: String
# The don't agree action by the current user against the parent entity with this reason.
current_user: DontAgreeAction
}
@@ -1023,7 +1047,7 @@ input CreateFlagInput {
item_type: ACTION_ITEM_TYPE!
# The reason for flagging the item.
reason: String!
reason: FLAG_REASON
# An optional message sent with the flagging action by the user.
message: String
@@ -1062,9 +1086,6 @@ input CreateDontAgreeInput {
# The type of the item for which we are to create the don't agree.
item_type: ACTION_ITEM_TYPE!
# The reason for not agreeing with the item.
reason: String
# An optional message sent with the don't agree action by the user.
message: String
}
+18
View File
@@ -254,6 +254,24 @@ en:
loading_results: "Loading Results"
marketing: "This looks like an ad/marketing"
moderate_this_stream: "Moderate this stream"
flags:
reasons:
user:
username_offensive: "Offensive"
username_nolike: "Dislike"
username_impersonating: "Impersonation"
username_spam: "Spam"
username_other: "Other"
comment:
comment_offensive: "Offensive"
comment_spam: "Spam"
comment_noagree: "Disagree"
comment_other: "Other"
suspect_word: "Suspect Word"
banned_word: "Banned Word"
body_count: "Body exceeds max length"
trust: "Trust"
links: "Link"
modqueue:
account: "account flags"
actions: Actions
+13
View File
@@ -249,6 +249,19 @@ es:
loading_results: "Cargando Resultados"
marketing: "Esto parece una propaganda"
moderate_this_stream: "Moderar este hilo de comentarios"
flags:
reasons:
user:
username_offensive: "Es ofensivo"
username_nolike: "No le gusta"
username_impersonating: "Está suplantando identidad"
username_spam: "Contiene spam"
username_other: "Otra razón"
comment:
comment_offensive: "Es ofensivo"
comment_spam: "Contiene spam"
comment_noagree: "No está de acuerdo"
comment_other: "Otra razón"
modqueue:
account: "reportes de cuentas"
actions: Acciones
+50
View File
@@ -0,0 +1,50 @@
const ActionModel = require('../models/action');
const mapping = {
COMMENTS: {
'Comment contains toxic language': 'TOXIC_COMMENT',
'Matched suspect word filter': 'SUSPECT_WORD',
'other': 'COMMENT_OTHER',
'Other': 'COMMENT_OTHER',
'This looks like an ad/marketing': 'COMMENT_SPAM',
'This comment is offensive': 'COMMENT_OFFENSIVE',
},
};
module.exports = {
async up() {
// Setup the batch operation.
const batch = ActionModel.collection.initializeUnorderedBulkOp();
for (const item_type in mapping) {
const mappings = mapping[item_type];
for (const OLD_GROUP_ID in mappings) {
const NEW_GROUP_ID = mappings[OLD_GROUP_ID];
// OLD
// {
// group_id: <OLD_GROUP_ID>
// }
// NEW
// {
// group_id: <NEW_GROUP_ID>
// }
batch.find({
group_id: OLD_GROUP_ID,
item_type,
}).update({
$set: {
group_id: NEW_GROUP_ID,
},
});
}
}
// Execute the batch update operation.
await batch.execute();
}
};
+21
View File
@@ -0,0 +1,21 @@
const ActionModel = require('../models/action');
module.exports = {
async up() {
// This will update all the old flags that are 'COMMENT_NOAGREE' to change
// them to DONTAGREE actions instead.
return ActionModel.update({
action_type: 'FLAG',
group_id: 'COMMENT_NOAGREE',
}, {
$set: {
action_type: 'DONTAGREE',
group_id: null,
},
}, {
multi: true,
});
}
};
+1 -1
View File
@@ -33,7 +33,7 @@
},
"talk": {
"migration": {
"minVersion": 1496771633
"minVersion": 1507322128
}
},
"repository": {
@@ -57,27 +57,23 @@ module.exports = {
hooks: {
RootMutation: {
addTag: {
async post(obj, {tag: {name, id, item_type}}, {user, mutators: {Comment}, pubsub}, info, result) {
async post(obj, {tag: {name, id, item_type}}, {user, mutators: {Comment}, pubsub}, _info) {
if (name === 'FEATURED' && item_type === 'COMMENTS') {
const comment = await Comment.setStatus({id: id, status: 'ACCEPTED'});
if (comment) {
pubsub.publish('commentFeatured', {comment, user});
}
return result;
}
return result;
},
},
removeTag: {
async post(obj, {tag: {name, id, item_type}}, {user, loaders: {Comments}, pubsub}, info, result) {
async post(obj, {tag: {name, id, item_type}}, {user, loaders: {Comments}, pubsub}, _info) {
if (name === 'FEATURED' && item_type === 'COMMENTS') {
const comment = await Comments.get.load(id);
if (comment) {
pubsub.publish('commentUnfeatured', {comment, user});
}
return result;
}
return result;
},
},
},
@@ -35,7 +35,7 @@ class FlagDetails extends Component {
<ul className={styles.info}>
{reasons.map((reason) =>
<li key={reason} className={styles.lessDetail}>
{reason} {summaries[reason].userFlagged && `(${summaries[reason].count})`}
{t(`flags.reasons.comment.${reason.toLowerCase()}`)} {summaries[reason].userFlagged && `(${summaries[reason].count})`}
</li>
)}
</ul>
@@ -1,5 +1,6 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {t} from 'plugin-api/beta/client/services';
import styles from './UserFlagDetails.css';
class UserFlagDetails extends Component {
@@ -25,7 +26,7 @@ class UserFlagDetails extends Component {
{Object.keys(summaries)
.map((reason) => (
<li key={reason}>
{reason} ({summaries[reason].count})
{t(`flags.reasons.comment.${reason.toLowerCase()}`)} ({summaries[reason].count})
<ul className={styles.subDetail}>
{summaries[reason].actions.map((action) =>
<li key={action.user.id}>
@@ -4,12 +4,20 @@ en:
Are you sure? The language in this comment might violate our community guidelines.
You can edit the comment or submit it for moderator review.
talk-plugin-toxic-comments:
unlikely: Unlikely
highly_likely: Highly Likely
possibly: Possibly
likely: Likely
toxic_comment: Toxic Comment
unlikely: "Unlikely"
highly_likely: "Highly Likely"
possibly: "Possibly"
likely: "Likely"
toxic_comment: "Toxic Comment"
still_toxic: |
This edited comment might still violate our community guidelines.
Our moderation team will review your comment shortly.
flags:
reasons:
comment:
toxic_comment: "Highly likely to be Toxic"
es:
flags:
reasons:
comment:
toxic_comment: "Muy probable que sea tóxico"
+2
View File
@@ -23,5 +23,7 @@ module.exports = {
'moderationContainer': '.talk-admin-moderation-container',
'drawerButton': '.mdl-layout__drawer-button',
'drawerOverlay': 'div.mdl-layout__obfuscator.is-visible',
'settingsButton': '.talk-admin-header-settings-button',
'signOutButton': '.talk-admin-header-sign-out',
}
};
+22
View File
@@ -0,0 +1,22 @@
module.exports = {
commands: [{
url: function() {
return `${this.api.launchUrl}/admin/community`;
},
ready() {
return this
.waitForElementVisible('body');
},
}],
elements: {
container: '.talk-admin-community',
flaggedAccountsContainer: '.talk-adnin-community-flagged-accounts',
flaggedUser:'.talk-admin-community-flagged-user',
flaggedUserApproveButton: '.talk-admin-flagged-user-approve-button',
flaggedUserRejectButton: '.talk-admin-flagged-user-reject-button',
usernameDialog: '.talk-reject-username-dialog',
usernameDialogButtons: '.talk-reject-username-dialog-buttons',
usernameDialogSuspend: '.talk-reject-username-dialog-button-k',
usernameDialogSuspensionMessage: '.talk-reject-username-dialog-suspension-message'
}
};
+16 -2
View File
@@ -48,10 +48,24 @@ module.exports = {
signInButton: '#coralSignInButton',
commentBoxTextarea: '#commentText',
commentBoxPostButton: '.talk-plugin-commentbox-button',
firstComment: '.talk-stream-comment.talk-stream-comment-level-0',
firstCommentContent: '.talk-stream-comment.talk-stream-comment-level-0 .talk-stream-comment-content',
respectButton: '.talk-stream-comment.talk-stream-comment-level-0 .talk-stream-comment-footer .talk-plugin-respect-button'
flagButton: '.talk-stream-comment.talk-stream-comment-level-0 .talk-plugin-flags-button',
respectButton: '.talk-stream-comment.talk-stream-comment-level-0 .talk-stream-comment-footer .talk-plugin-respect-button',
restrictedMessageBox: '.talk-restricted-message-box',
suspendedAccountInput: '.talk-suspended-account-username-input',
suspendedAccountSubmitButton: '.talk-suspended-account-submit-button',
},
sections: {
flag: {
selector: '.talk-plugin-flags-popup',
elements: {
offensiveUsernameRadio: '.talk-plugin-flags-popup-radio#USERNAME_OFFENSIVE',
flagUsernameRadio: '.talk-plugin-flags-popup-radio#USERS',
continueButton: '.talk-plugin-flags-popup-button',
popUpText: '.talk-plugin-flags-popup-text',
}
},
profile: {
selector: '.talk-embed-stream-profile-tab-pane',
elements: {
@@ -59,7 +73,7 @@ module.exports = {
myCommentHistory: '.talk-my-profile-comment-history',
myCommentHistoryReactions: '.talk-my-profile-comment-history .comment-summary .comment-summary-reactions',
myCommentHistoryReactionCount: '.talk-my-profile-comment-history .comment-summary .comment-summary-reactions .comment-summary-reaction-count',
myCommentHistoryComment: '.talk-my-profile-comment-history .my-comment-body'
myCommentHistoryComment: '.talk-my-profile-comment-history .my-comment-body',
},
},
comments: {
+1
View File
@@ -49,6 +49,7 @@ module.exports = {
.click('@drawerOverlay')
.waitForElementVisible('@communitySection');
},
after: (client) => {
client.end();
}
+157
View File
@@ -0,0 +1,157 @@
module.exports = {
'admin logs in': (client) => {
const adminPage = client.page.admin();
const {testData: {admin}} = client.globals;
adminPage
.navigate()
.waitForElementVisible('@loginLayout')
.waitForElementVisible('@signInForm')
.setValue('@emailInput', admin.email)
.setValue('@passwordInput', admin.password)
.waitForElementVisible('@signInButton')
.click('@signInButton');
client.pause(3000);
adminPage
.waitForElementVisible('@moderationContainer');
},
'admin flags user\'s username as offensive': (client) => {
const embedStream = client.page.embedStream();
const flagSection = client.page.embedStream().section.embed.section.flag;
const embed = embedStream
.navigate()
.getEmbedSection();
embed
.waitForElementVisible('@firstComment')
.waitForElementVisible('@flagButton')
.click('@flagButton');
flagSection
.waitForElementVisible('@flagUsernameRadio')
.click('@flagUsernameRadio')
.waitForElementVisible('@continueButton')
.click('@continueButton')
.waitForElementVisible('@offensiveUsernameRadio')
.click('@offensiveUsernameRadio')
.click('@continueButton')
.waitForElementVisible('@popUpText')
.click('@continueButton');
},
'admin goes to Reported Usernames': (client) => {
const community = client.page.adminCommunity();
community
.navigate();
community
.waitForElementVisible('@container')
.waitForElementVisible('@flaggedAccountsContainer')
.waitForElementVisible('@flaggedUser');
},
'admin rejects the user flag': (client) => {
const community = client.page.adminCommunity();
community
.waitForElementVisible('@flaggedUserRejectButton')
.click('@flaggedUserRejectButton');
},
'admin suspends the user': (client) => {
const community = client.page.adminCommunity();
community
.waitForElementVisible('@usernameDialog')
.waitForElementVisible('@usernameDialogButtons')
.waitForElementVisible('@usernameDialogSuspend')
.click('@usernameDialogSuspend')
.waitForElementVisible('@usernameDialogSuspensionMessage')
.click('@usernameDialogSuspend')
.waitForElementNotPresent('@flaggedUser');
},
'admin logs out': (client) => {
const admin = client.page.admin();
admin
.waitForElementVisible('@settingsButton')
.click('@settingsButton')
.waitForElementVisible('@signOutButton')
.click('@signOutButton');
},
'user logs in': (client) => {
const {testData: {user}} = client.globals;
const embedStream = client.page.embedStream();
const embed = embedStream
.navigate()
.getEmbedSection();
embed
.waitForElementVisible('@signInButton')
.click('@signInButton');
client.pause(3000);
// Focusing on the Login PopUp
client.windowHandles((result) => {
const handle = result.value[1];
client.switchWindow(handle);
});
const login = client.page.login();
login
.setValue('@emailInput', user.email)
.setValue('@passwordInput', user.password)
.waitForElementVisible('@signIn')
.waitForElementVisible('@loginButton')
.click('@loginButton');
// Focusing on the Embed Window
client.windowHandles((result) => {
const handle = result.value[0];
client.switchWindow(handle);
});
},
'user account is suspended, should see restricted message box': (client) => {
const embedStream = client.page.embedStream();
const embed = embedStream
.navigate()
.getEmbedSection();
embed
.waitForElementVisible('@restrictedMessageBox');
},
'user picks another username': (client) => {
const {testData: {user}} = client.globals;
const embedStream = client.page.embedStream();
const embed = embedStream
.navigate()
.getEmbedSection();
embed
.waitForElementVisible('@suspendedAccountInput')
.setValue('@suspendedAccountInput', `${user.username}-alternative`)
.waitForElementVisible('@suspendedAccountSubmitButton')
.click('@suspendedAccountSubmitButton');
},
'user should not be able to comment': (client) => {
const embedStream = client.page.embedStream();
const embed = embedStream
.navigate()
.getEmbedSection();
embed
.waitForElementNotPresent('@commentBoxTextarea')
.waitForElementNotPresent('@commentBoxPostButton');
},
after: (client) => {
client.end();
}
};