diff --git a/.eslintrc.json b/.eslintrc.json
index d00d21106..d2356ace2 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -2,5 +2,10 @@
"env": {
"jest": true
},
+ "settings": {
+ "react": {
+ "version": "15.0"
+ }
+ },
"extends": "@coralproject/eslint-config-talk"
}
diff --git a/.nsprc b/.nsprc
index 530a17a9f..8962db719 100644
--- a/.nsprc
+++ b/.nsprc
@@ -7,6 +7,7 @@
"https://nodesecurity.io/advisories/594",
"https://nodesecurity.io/advisories/603",
"https://nodesecurity.io/advisories/611",
- "https://nodesecurity.io/advisories/612"
+ "https://nodesecurity.io/advisories/612",
+ "https://nodesecurity.io/advisories/654"
]
}
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 000000000..2f954597a
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1,2 @@
+8
+
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 000000000..10041faa8
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,4 @@
+overrides:
+- files: "bin/cli*"
+ options:
+ parser: babylon
diff --git a/Dockerfile.onbuild b/Dockerfile.onbuild
index 3e837aad6..85becf461 100644
--- a/Dockerfile.onbuild
+++ b/Dockerfile.onbuild
@@ -7,6 +7,7 @@ ONBUILD ARG TALK_REPLY_COMMENTS_LOAD_DEPTH=3
ONBUILD ARG TALK_THREADING_LEVEL=3
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
ONBUILD ARG TALK_DEFAULT_LANG=en
+ONBUILD ARG TALK_WHITELISTED_LANGUAGES
ONBUILD ARG TALK_PLUGINS_JSON
ONBUILD ARG TALK_WEBPACK_SOURCE_MAP
@@ -20,4 +21,4 @@ ONBUILD COPY . /usr/src/app
ONBUILD RUN cli plugins reconcile && \
yarn && \
yarn build && \
- yarn cache clean
\ No newline at end of file
+ yarn cache clean
diff --git a/app.js b/app.js
index c5507da2e..b3400b939 100644
--- a/app.js
+++ b/app.js
@@ -1,4 +1,6 @@
const express = require('express');
+const nunjucks = require('nunjucks');
+const cons = require('consolidate');
const trace = require('./middleware/trace');
const logging = require('./middleware/logging');
const path = require('path');
@@ -72,7 +74,26 @@ app.use(
// VIEW CONFIGURATION
//==============================================================================
-app.set('views', path.join(__dirname, 'views'));
+// configure the default views directory.
+const views = path.join(__dirname, 'views');
+app.set('views', views);
+
+// reconfigure nunjucks.
+cons.requires.nunjucks = nunjucks.configure(views, {
+ autoescape: true,
+ trimBlocks: true,
+ lstripBlocks: true,
+ watch: process.env.NODE_ENV === 'development',
+});
+
+// assign the nunjucks engine to .njk files.
+app.engine('njk', cons.nunjucks);
+
+// assign the ejs engine to .ejs and .html files.
+app.engine('ejs', cons.ejs);
+app.engine('html', cons.ejs);
+
+// set .ejs as the default extension.
app.set('view engine', 'ejs');
//==============================================================================
diff --git a/app.json b/app.json
index 6c85e3cc2..9f245105f 100644
--- a/app.json
+++ b/app.json
@@ -18,6 +18,10 @@
"value": "",
"required": true
},
+ "MAILGUN_SMTP_PASSWORD": {
+ "value": "",
+ "required": true
+ },
"NODE_ENV": "production",
"REWRITE_ENV": "TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:MAILGUN_SMTP_SERVER,TALK_SMTP_PORT:MAILGUN_SMTP_PORT,TALK_SMTP_USERNAME:MAILGUN_SMTP_LOGIN,TALK_SMTP_PASSWORD:MAILGUN_SMTP_PASSWORD",
"NPM_CONFIG_PRODUCTION": "false"
diff --git a/bin/cli-assets b/bin/cli-assets
index 5c1afb741..8242a02d6 100755
--- a/bin/cli-assets
+++ b/bin/cli-assets
@@ -7,7 +7,7 @@
const util = require('./util');
const program = require('commander');
const parseDuration = require('ms');
-const Table = require('cli-table');
+const Table = require('cli-table2');
const AssetModel = require('../models/asset');
const CommentModel = require('../models/comment');
const AssetsService = require('../services/assets');
diff --git a/bin/cli-plugins b/bin/cli-plugins
index f2e76de21..85132c39d 100755
--- a/bin/cli-plugins
+++ b/bin/cli-plugins
@@ -329,8 +329,7 @@ async function createSeedPlugin() {
if (answers.addPluginsJson) {
const pluginsJson = path.resolve(__dirname, '..', 'plugins.json');
- fs
- .readJson(pluginsJson)
+ fs.readJson(pluginsJson)
.then(j => {
// This is a client-side plugin, let's push this.
if (answers.client) {
diff --git a/bin/cli-token b/bin/cli-token
index b131728e6..2590ec106 100755
--- a/bin/cli-token
+++ b/bin/cli-token
@@ -8,7 +8,7 @@ const util = require('./util');
const program = require('commander');
const mongoose = require('../services/mongoose');
const TokensService = require('../services/tokens');
-const Table = require('cli-table');
+const Table = require('cli-table2');
// Register the shutdown criteria.
util.onshutdown([() => mongoose.disconnect()]);
@@ -49,7 +49,10 @@ async function revokeToken(tokenID) {
async function createToken(userID, tokenName) {
try {
- let { pat: { id }, jwt } = await TokensService.create(userID, tokenName);
+ let {
+ pat: { id },
+ jwt,
+ } = await TokensService.create(userID, tokenName);
console.log(`Created Token[${id}] for User[${userID}] = ${jwt}`);
diff --git a/bin/cli-users b/bin/cli-users
index bf34cce97..743119a0a 100755
--- a/bin/cli-users
+++ b/bin/cli-users
@@ -8,7 +8,7 @@ const util = require('./util');
const program = require('commander');
const inquirer = require('inquirer');
const { stripIndent } = require('common-tags');
-const Table = require('cli-table');
+const Table = require('cli-table2');
// Make things colorful!
require('colors');
diff --git a/client/coral-admin/src/actions/rejectUsernameDialog.js b/client/coral-admin/src/actions/rejectUsernameDialog.js
new file mode 100644
index 000000000..946b36ef7
--- /dev/null
+++ b/client/coral-admin/src/actions/rejectUsernameDialog.js
@@ -0,0 +1,14 @@
+import {
+ SHOW_REJECT_USERNAME_DIALOG,
+ HIDE_REJECT_USERNAME_DIALOG,
+} from '../constants/rejectUsernameDialog';
+
+export const showRejectUsernameDialog = ({ userId, username }) => ({
+ type: SHOW_REJECT_USERNAME_DIALOG,
+ userId,
+ username,
+});
+
+export const hideRejectUsernameDialog = () => ({
+ type: HIDE_REJECT_USERNAME_DIALOG,
+});
diff --git a/client/coral-admin/src/components/ActionsMenu.js b/client/coral-admin/src/components/ActionsMenu.js
index 81b857a13..52370f9a7 100644
--- a/client/coral-admin/src/components/ActionsMenu.js
+++ b/client/coral-admin/src/components/ActionsMenu.js
@@ -76,7 +76,7 @@ ActionsMenu.propTypes = {
icon: PropTypes.string,
children: PropTypes.node,
className: PropTypes.string,
- label: PropTypes.string,
+ label: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
buttonClassNames: PropTypes.string,
};
diff --git a/client/coral-admin/src/components/ActionsMenuItem.js b/client/coral-admin/src/components/ActionsMenuItem.js
index 7370ba466..54816147a 100644
--- a/client/coral-admin/src/components/ActionsMenuItem.js
+++ b/client/coral-admin/src/components/ActionsMenuItem.js
@@ -15,7 +15,7 @@ const ActionsMenuItem = props => (
ActionsMenuItem.propTypes = {
className: PropTypes.string,
- children: PropTypes.string,
+ children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
};
export default ActionsMenuItem;
diff --git a/client/coral-admin/src/components/AdminLogin.js b/client/coral-admin/src/components/AdminLogin.js
index 99bd61696..e6bcb3b89 100644
--- a/client/coral-admin/src/components/AdminLogin.js
+++ b/client/coral-admin/src/components/AdminLogin.js
@@ -5,6 +5,7 @@ import styles from './NotFound.css';
import { Button, TextField, Alert, Success } from 'coral-ui';
import Recaptcha from 'react-recaptcha';
import cn from 'classnames';
+import t from 'coral-framework/services/i18n';
class AdminLogin extends React.Component {
constructor(props) {
@@ -41,13 +42,13 @@ class AdminLogin extends React.Component {
{errorMessage && {errorMessage}}
this.setState({ email: e.target.value })}
/>
this.setState({ password: e.target.value })}
type="password"
@@ -60,10 +61,10 @@ class AdminLogin extends React.Component {
full
onClick={this.handleSignIn}
>
- Sign In
+ {t('login.sign_in_button')}
- Forgot your password?{' '}
+ {t('login.forgot_password')}{' '}
- Request a new one.
+ {t('login.request_passowrd')}
{loginMaxExceeded && (
@@ -95,14 +96,14 @@ class AdminLogin extends React.Component {
>
{this.props.passwordRequestSuccess}{' '}
- Sign in
+ {t('login.sign_in')}
) : (
@@ -64,22 +94,56 @@ class UserDetail extends React.Component {
);
}
- getActionMenuLabel() {
- const { root: { user } } = this.props;
+ getActionMenuLabel(user) {
+ const userStatusArr = getUserStatusArray(user);
+ const count = userStatusArr.length;
- if (isBanned(user)) {
- return 'Banned';
- } else if (isSuspended(user)) {
- return 'Suspended';
+ if (count > 1) {
+ return `Status (${count})`;
+ } else {
+ const activeStatus = userStatusArr[0];
+ switch (activeStatus) {
+ case 'suspended':
+ return t('user_detail.suspended');
+ case 'banned':
+ return t('user_detail.banned');
+ case 'usernameRejected':
+ return (
+
+ {t('user_detail.username')}
+ {` `}
+
+
+ );
+ case 'usernameChanged':
+ return (
+
+ {t('user_detail.username')}
+ {` `}
+
+
+ );
+ default:
+ return activeStatus;
+ }
}
-
- return '';
}
+ goToReportedUsernames = () => {
+ const { router } = this.props;
+ router.push('/admin/community/flagged');
+ };
+
renderLoaded() {
const {
root,
- root: { me, user, totalComments, rejectedComments },
+ root: {
+ me,
+ user,
+ totalComments,
+ rejectedComments,
+ settings: { karmaThresholds },
+ },
activeTab,
selectedCommentIds,
toggleSelect,
@@ -97,7 +161,7 @@ class UserDetail extends React.Component {
} = this.props;
// if totalComments is 0, you're dividing by zero
- let rejectedPercent = rejectedComments / totalComments * 100;
+ let rejectedPercent = (rejectedComments / totalComments) * 100;
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
rejectedPercent = 0;
@@ -105,6 +169,8 @@ class UserDetail extends React.Component {
const banned = isBanned(user);
const suspended = isSuspended(user);
+ const usernameRejected = isUsernameRejected(user);
+ const usernameChanged = isUsernameChanged(user);
const slotPassthrough = {
root,
@@ -137,7 +203,7 @@ class UserDetail extends React.Component {
},
'talk-admin-user-detail-actions-button'
)}
- label={this.getActionMenuLabel()}
+ label={this.getActionMenuLabel(user)}
>
{suspended ? (
unsuspendUser({ id: user.id })}>
@@ -164,6 +230,27 @@ class UserDetail extends React.Component {
{t('user_detail.ban')}
)}
+
+ {usernameChanged && (
+
+ {t('user_detail.username_needs_approval')}
+ {` `}
+
+
+ )}
+
+ {usernameRejected && !usernameChanged ? (
+
+ {t('user_detail.username_rejected')}
+
+ ) : (
+
+ {t('user_detail.reject_username')}
+
+ )}
)}
@@ -177,7 +264,19 @@ class UserDetail extends React.Component {
- -
+
-
+
+
+ {t('user_detail.id')}:
+
+ {user.id}{' '}
+
+
+ -
{t('user_detail.member_since')}:
@@ -185,20 +284,35 @@ class UserDetail extends React.Component {
{new Date(user.created_at).toLocaleString()}
- {user.profiles.map(({ id }) => (
- -
-
-
- {t('user_detail.email')}:
-
- {id}{' '}
-
-
- ))}
+ -
+
+
+ {t('user_detail.email')}:
+
+ {user.email}{' '}
+
+
+
+ {user.profiles
+ .filter(filterOutLocalProfiles)
+ .map(({ provider, id }) => (
+ -
+
+
+ {capitalize(provider)} {t('user_detail.id')}:
+
+ {id}{' '}
+
+
+ ))}
@@ -339,6 +456,7 @@ class UserDetail extends React.Component {
}
UserDetail.propTypes = {
+ router: PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
hideUserDetail: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
@@ -355,11 +473,13 @@ UserDetail.propTypes = {
selectedCommentIds: PropTypes.array.isRequired,
viewUserDetail: PropTypes.any.isRequired,
loadMore: PropTypes.any.isRequired,
+ showRejectUsernameDialog: PropTypes.func,
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
modal: PropTypes.bool,
+ rejectUsername: PropTypes.func.isRequired,
};
export default UserDetail;
diff --git a/client/coral-admin/src/components/UserDetailComment.js b/client/coral-admin/src/components/UserDetailComment.js
index 41fb87b28..e8472219a 100644
--- a/client/coral-admin/src/components/UserDetailComment.js
+++ b/client/coral-admin/src/components/UserDetailComment.js
@@ -114,6 +114,7 @@ class UserDetailComment extends React.Component {
className={styles.external}
href={`${comment.asset.url}?commentId=${comment.id}`}
target="_blank"
+ rel="noopener noreferrer"
>
{t('comment.view_context')}
diff --git a/client/coral-admin/src/components/UserDetailCommentList.js b/client/coral-admin/src/components/UserDetailCommentList.js
index 3a31ee3f9..3a5c7462d 100644
--- a/client/coral-admin/src/components/UserDetailCommentList.js
+++ b/client/coral-admin/src/components/UserDetailCommentList.js
@@ -10,7 +10,10 @@ import ApproveButton from './ApproveButton';
const UserDetailCommentList = props => {
const {
root,
- root: { user, comments: { nodes, hasNextPage } },
+ root: {
+ user,
+ comments: { nodes, hasNextPage },
+ },
acceptComment,
rejectComment,
selectedCommentIds,
diff --git a/client/coral-admin/src/constants/rejectUsernameDialog.js b/client/coral-admin/src/constants/rejectUsernameDialog.js
new file mode 100644
index 000000000..8ab48c106
--- /dev/null
+++ b/client/coral-admin/src/constants/rejectUsernameDialog.js
@@ -0,0 +1,2 @@
+export const SHOW_REJECT_USERNAME_DIALOG = 'SHOW_REJECT_USERNAME_DIALOG';
+export const HIDE_REJECT_USERNAME_DIALOG = 'HIDE_REJECT_USERNAME_DIALOG';
diff --git a/client/coral-admin/src/containers/BanUserDialog.js b/client/coral-admin/src/containers/BanUserDialog.js
index 3617902ff..e1b053337 100644
--- a/client/coral-admin/src/containers/BanUserDialog.js
+++ b/client/coral-admin/src/containers/BanUserDialog.js
@@ -12,7 +12,7 @@ import { compose } from 'react-apollo';
import t from 'coral-framework/services/i18n';
class BanUserDialogContainer extends Component {
- banUser = async () => {
+ banUser = async ({ message }) => {
const {
userId,
commentId,
@@ -21,7 +21,7 @@ class BanUserDialogContainer extends Component {
setCommentStatus,
hideBanUserDialog,
} = this.props;
- await banUser({ id: userId, message: '' });
+ await banUser({ id: userId, message });
hideBanUserDialog();
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
await setCommentStatus({ commentId, status: 'REJECTED' });
@@ -77,7 +77,10 @@ const mapDispatchToProps = dispatch => ({
});
export default compose(
- connect(mapStateToProps, mapDispatchToProps),
+ connect(
+ mapStateToProps,
+ mapDispatchToProps
+ ),
withBanUser,
withSetCommentStatus
)(BanUserDialogContainer);
diff --git a/client/coral-admin/src/containers/Layout.js b/client/coral-admin/src/containers/Layout.js
index 580b20f9b..22c624b7d 100644
--- a/client/coral-admin/src/containers/Layout.js
+++ b/client/coral-admin/src/containers/Layout.js
@@ -6,6 +6,7 @@ import Login from '../containers/Login';
import { FullLoading } from '../components/FullLoading';
import BanUserDialog from './BanUserDialog';
import SuspendUserDialog from './SuspendUserDialog';
+import RejectUsernameDialog from './RejectUsernameDialog';
import { toggleModal as toggleShortcutModal } from '../actions/moderation';
import { logout } from 'coral-framework/actions/auth';
import { can } from 'coral-framework/services/perms';
@@ -41,6 +42,7 @@ class LayoutContainer extends React.Component {
>
+
{children}
@@ -79,4 +81,7 @@ const mapDispatchToProps = dispatch =>
dispatch
);
-export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(LayoutContainer);
diff --git a/client/coral-admin/src/containers/RejectUsernameDialog.js b/client/coral-admin/src/containers/RejectUsernameDialog.js
new file mode 100644
index 000000000..ec996a9d5
--- /dev/null
+++ b/client/coral-admin/src/containers/RejectUsernameDialog.js
@@ -0,0 +1,92 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+import RejectUsernameDialog from '../components/RejectUsernameDialog';
+import { hideRejectUsernameDialog } from '../actions/rejectUsernameDialog';
+import {
+ withRejectUsername,
+ withPostFlag,
+} from 'coral-framework/graphql/mutations';
+import { notify } from 'coral-framework/actions/notification';
+import { compose } from 'react-apollo';
+import { getErrorMessages } from 'coral-framework/utils';
+
+class RejectUsernameDialogContainer extends Component {
+ rejectUsername = async ({ reason, message }) => {
+ const {
+ postFlag,
+ rejectUsername,
+ hideRejectUsernameDialog,
+ userId,
+ } = this.props;
+
+ // First flag the user.
+ try {
+ await postFlag({
+ item_id: userId,
+ item_type: 'USERS',
+ reason,
+ message,
+ });
+ } catch (error) {
+ // Ignore already exists error, otherwise show error.
+ if (
+ error.errors &&
+ (error.errors.length !== 1 ||
+ error.errors[0].translation_key !== 'ALREADY_EXISTS')
+ ) {
+ notify('error', getErrorMessages(error));
+ }
+ }
+
+ await rejectUsername(userId);
+ hideRejectUsernameDialog();
+ };
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+RejectUsernameDialogContainer.propTypes = {
+ rejectUsername: PropTypes.func.isRequired,
+ hideRejectUsernameDialog: PropTypes.func,
+ open: PropTypes.bool,
+ userId: PropTypes.string,
+ username: PropTypes.string,
+};
+
+const mapStateToProps = ({
+ rejectUsernameDialog: { open, userId, username },
+}) => ({
+ open,
+ userId,
+ username,
+});
+
+const mapDispatchToProps = dispatch => ({
+ ...bindActionCreators(
+ {
+ hideRejectUsernameDialog,
+ notify,
+ },
+ dispatch
+ ),
+});
+
+export default compose(
+ connect(
+ mapStateToProps,
+ mapDispatchToProps
+ ),
+ withRejectUsername,
+ withPostFlag({ notifyOnError: false })
+)(RejectUsernameDialogContainer);
diff --git a/client/coral-admin/src/containers/SignIn.js b/client/coral-admin/src/containers/SignIn.js
index 523d81091..365b32db3 100644
--- a/client/coral-admin/src/containers/SignIn.js
+++ b/client/coral-admin/src/containers/SignIn.js
@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
-import { withSignIn } from 'coral-framework/hocs';
+import { withSignIn, withPopupAuthHandler } from 'coral-framework/hocs';
import { compose } from 'recompose';
import SignIn from '../components/SignIn';
@@ -55,4 +55,7 @@ SignInContainer.propTypes = {
requireRecaptcha: PropTypes.bool.isRequired,
};
-export default compose(withSignIn)(SignInContainer);
+export default compose(
+ withSignIn,
+ withPopupAuthHandler
+)(SignInContainer);
diff --git a/client/coral-admin/src/containers/SuspendUserDialog.js b/client/coral-admin/src/containers/SuspendUserDialog.js
index f03d48f52..1d3d5d2c0 100644
--- a/client/coral-admin/src/containers/SuspendUserDialog.js
+++ b/client/coral-admin/src/containers/SuspendUserDialog.js
@@ -86,7 +86,10 @@ const mapDispatchToProps = dispatch => ({
});
export default compose(
- connect(mapStateToProps, mapDispatchToProps),
+ connect(
+ mapStateToProps,
+ mapDispatchToProps
+ ),
withSuspendUser,
withSetCommentStatus,
withOrganizationName
diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js
index 360819dd6..e9ffea4d1 100644
--- a/client/coral-admin/src/containers/UserDetail.js
+++ b/client/coral-admin/src/containers/UserDetail.js
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import UserDetail from '../components/UserDetail';
import withQuery from 'coral-framework/hocs/withQuery';
+import { withRouter } from 'react-router';
import {
getDefinitionName,
getSlotFragmentSpreads,
@@ -21,11 +22,14 @@ import {
withSetCommentStatus,
withUnbanUser,
withUnsuspendUser,
+ withRejectUsername,
+ withPostFlag,
} from 'coral-framework/graphql/mutations';
import UserDetailComment from './UserDetailComment';
import update from 'immutability-helper';
import { showBanUserDialog } from 'actions/banUserDialog';
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
+import { showRejectUsernameDialog } from 'actions/rejectUsernameDialog';
const commentConnectionFragment = gql`
fragment CoralAdmin_UserDetail_CommentConnection on CommentConnection {
@@ -131,6 +135,7 @@ class UserDetailContainer extends React.Component {
loading={loading}
error={this.props.data && this.props.data.error}
loadMore={this.loadMore}
+ rejectUsername={this.props.rejectUsername}
{...this.props}
/>
);
@@ -148,6 +153,7 @@ UserDetailContainer.propTypes = {
selectedCommentIds: PropTypes.array,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
+ rejectUsername: PropTypes.func.isRequired,
userId: PropTypes.string,
};
@@ -179,12 +185,14 @@ export const withUserDetailQuery = withQuery(
id
username
created_at
+ email
profiles {
id
provider
}
reliable {
- flagger
+ commenter
+ commenterKarma
}
state {
status {
@@ -225,6 +233,14 @@ export const withUserDetailQuery = withQuery(
}
${getSlotFragmentSpreads(slots, 'user')}
}
+ settings {
+ karmaThresholds {
+ comment {
+ reliable
+ unreliable
+ }
+ }
+ }
me {
id
}
@@ -265,6 +281,7 @@ const mapDispatchToProps = dispatch => ({
{
showBanUserDialog,
showSuspendUserDialog,
+ showRejectUsernameDialog,
changeTab,
clearUserDetailSelections,
toggleSelectCommentInUserDetail,
@@ -277,9 +294,15 @@ const mapDispatchToProps = dispatch => ({
});
export default compose(
- connect(mapStateToProps, mapDispatchToProps),
+ connect(
+ mapStateToProps,
+ mapDispatchToProps
+ ),
withUserDetailQuery,
withSetCommentStatus,
withUnbanUser,
- withUnsuspendUser
+ withUnsuspendUser,
+ withRejectUsername,
+ withPostFlag,
+ withRouter
)(UserDetailContainer);
diff --git a/client/coral-admin/src/graphql/index.js b/client/coral-admin/src/graphql/index.js
index 3874e6c94..9fca9f497 100644
--- a/client/coral-admin/src/graphql/index.js
+++ b/client/coral-admin/src/graphql/index.js
@@ -24,6 +24,25 @@ const userRoleFragment = gql`
}
`;
+/**
+ * calculateReliability will determine the reliability of a karma score based on
+ * the settings for the karma type.
+ *
+ * @param {Number} karma - the current karma value/score for the given user
+ * @param {Object} thresholds - the karma thresholds to base the karma computation on
+ */
+const calculateReliability = (karma, { reliable, unreliable }) => {
+ if (karma >= reliable) {
+ return true;
+ }
+
+ if (karma <= unreliable) {
+ return false;
+ }
+
+ return null;
+};
+
export default {
mutations: {
SetUserRole: ({ variables: { id, role } }) => ({
@@ -47,7 +66,11 @@ export default {
});
},
}),
- SuspendUser: ({ variables: { input: { id, until } } }) => ({
+ SuspendUser: ({
+ variables: {
+ input: { id, until },
+ },
+ }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
@@ -73,7 +96,11 @@ export default {
});
},
}),
- UnsuspendUser: ({ variables: { input: { id } } }) => ({
+ UnsuspendUser: ({
+ variables: {
+ input: { id },
+ },
+ }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
@@ -98,7 +125,11 @@ export default {
});
},
}),
- BanUser: ({ variables: { input: { id } } }) => ({
+ BanUser: ({
+ variables: {
+ input: { id },
+ },
+ }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
@@ -123,7 +154,11 @@ export default {
});
},
}),
- UnbanUser: ({ variables: { input: { id } } }) => ({
+ UnbanUser: ({
+ variables: {
+ input: { id },
+ },
+ }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
@@ -156,7 +191,9 @@ export default {
}
const updated = update(prev, {
users: {
- nodes: { $apply: nodes => nodes.filter(node => node.id !== id) },
+ nodes: {
+ $apply: nodes => nodes.filter(node => node.id !== id),
+ },
},
});
return updated;
@@ -173,6 +210,12 @@ export default {
},
updateQueries: {
TalkAdmin_Community_FlaggedAccounts: (prev, { mutationResult }) => {
+ // No need to update, when user was not in the flagged users queue.
+ // TODO: this should be more generic, e.g. looking at the history.
+ if (!prev.flaggedUsers.nodes.find(node => node.id === id)) {
+ return prev;
+ }
+
const decrement = {
flaggedUsernamesCount: { $apply: count => count - 1 },
};
@@ -185,41 +228,14 @@ export default {
const updated = update(prev, {
...decrement,
flaggedUsers: {
- nodes: { $apply: nodes => nodes.filter(node => node.id !== id) },
+ nodes: {
+ $apply: nodes => nodes.filter(node => node.id !== id),
+ },
},
});
return updated;
},
},
- update: proxy => {
- proxy.writeFragment({
- fragment: gql`
- fragment Talk_ApproveUsername on User {
- state {
- status {
- username {
- status
- }
- }
- }
- }
- `,
- id: `User_${id}`,
- data: {
- __typename: 'User',
- state: {
- __typename: 'UserState',
- status: {
- __typename: 'UserStatus',
- username: {
- __typename: 'UsernameStatus',
- status: 'APPROVED',
- },
- },
- },
- },
- });
- },
}),
RejectUsername: ({ variables: { id } }) => ({
optimisticResponse: {
@@ -231,6 +247,12 @@ export default {
},
updateQueries: {
TalkAdmin_Community_FlaggedAccounts: (prev, { mutationResult }) => {
+ // No need to update, when user was not in the flagged users queue.
+ // TODO: this should be more generic, e.g. looking at the history.
+ if (!prev.flaggedUsers.nodes.find(node => node.id === id)) {
+ return prev;
+ }
+
const decrement = {
flaggedUsernamesCount: { $apply: count => count - 1 },
};
@@ -251,35 +273,6 @@ export default {
return updated;
},
},
- update: proxy => {
- proxy.writeFragment({
- fragment: gql`
- fragment Talk_RejectUsername on User {
- state {
- status {
- username {
- status
- }
- }
- }
- }
- `,
- id: `User_${id}`,
- data: {
- __typename: 'User',
- state: {
- __typename: 'UserState',
- status: {
- __typename: 'UserStatus',
- username: {
- __typename: 'UsernameStatus',
- status: 'REJECTED',
- },
- },
- },
- },
- });
- },
}),
UpdateSettings: ({ variables: { input } }) => ({
updateQueries: {
@@ -295,12 +288,38 @@ export default {
updateQueries: {
CoralAdmin_UserDetail: prev => {
const increment = {
+ user: {
+ reliable: {
+ commenter: {
+ $set: calculateReliability(
+ prev.user.reliable.commenterKarma - 1,
+ prev.settings.karmaThresholds.comment
+ ),
+ },
+ commenterKarma: {
+ $apply: count => count - 1,
+ },
+ },
+ },
rejectedComments: {
$apply: count => (count < prev.totalComments ? count + 1 : count),
},
};
const decrement = {
+ user: {
+ reliable: {
+ commenter: {
+ $set: calculateReliability(
+ prev.user.reliable.commenterKarma + 1,
+ prev.settings.karmaThresholds.comment
+ ),
+ },
+ commenterKarma: {
+ $apply: count => count + 1,
+ },
+ },
+ },
rejectedComments: {
$apply: count => (count > 0 ? count - 1 : 0),
},
diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js
index be0a8a606..e806f5d11 100644
--- a/client/coral-admin/src/reducers/index.js
+++ b/client/coral-admin/src/reducers/index.js
@@ -5,10 +5,12 @@ import moderation from './moderation';
import install from './install';
import banUserDialog from './banUserDialog';
import suspendUserDialog from './suspendUserDialog';
+import rejectUsernameDialog from './rejectUsernameDialog';
import userDetail from './userDetail';
import ui from './ui';
export default {
+ rejectUsernameDialog,
banUserDialog,
configure,
suspendUserDialog,
diff --git a/client/coral-admin/src/reducers/rejectUsernameDialog.js b/client/coral-admin/src/reducers/rejectUsernameDialog.js
new file mode 100644
index 000000000..2ed31066b
--- /dev/null
+++ b/client/coral-admin/src/reducers/rejectUsernameDialog.js
@@ -0,0 +1,29 @@
+import {
+ SHOW_REJECT_USERNAME_DIALOG,
+ HIDE_REJECT_USERNAME_DIALOG,
+} from '../constants/rejectUsernameDialog';
+
+const initialState = {
+ open: false,
+ userId: null,
+ username: '',
+};
+
+export default function rejectUsernameDialog(state = initialState, action) {
+ switch (action.type) {
+ case SHOW_REJECT_USERNAME_DIALOG:
+ return {
+ ...state,
+ open: true,
+ userId: action.userId,
+ username: action.username,
+ };
+ case HIDE_REJECT_USERNAME_DIALOG:
+ return {
+ ...state,
+ open: 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 669da480d..ac565c8f8 100644
--- a/client/coral-admin/src/routes/Community/components/People.css
+++ b/client/coral-admin/src/routes/Community/components/People.css
@@ -133,6 +133,7 @@ th.header:nth-child(2), th.header:nth-child(3) {
.roleDropdown {
width: 150px;
+ text-align: left;
}
.roleOption {
diff --git a/client/coral-admin/src/routes/Community/components/People.js b/client/coral-admin/src/routes/Community/components/People.js
index 5645f88f1..d152f83e3 100644
--- a/client/coral-admin/src/routes/Community/components/People.js
+++ b/client/coral-admin/src/routes/Community/components/People.js
@@ -130,7 +130,9 @@ class People extends React.Component {
{user.username}
- {user.profiles.map(({ id }) => id)}
+ {user.email
+ ? user.email
+ : user.profiles.map(p => p.id).join(', ')}
@@ -200,7 +202,7 @@ class People extends React.Component {
|
@@ -101,7 +101,7 @@ class RejectUsernameDialog extends Component {
@@ -110,7 +110,7 @@ class RejectUsernameDialog extends Component {
{Object.keys(stages[stage].options).map((key, i) => (
@@ -118,7 +118,7 @@ class RejectUsernameDialog extends Component {
key={i}
className={cn(
'talk-admin-username-dialog-button',
- `talk-admin-reject-username-dialog-button-${key}`
+ `talk-admin-reject-reported-username-dialog-button-${key}`
)}
onClick={this.onActionClick(stage, i)}
>
diff --git a/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js b/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js
index 60ae4ee99..328030390 100644
--- a/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js
+++ b/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js
@@ -46,7 +46,11 @@ class FlaggedAccountsContainer extends Component {
document: USERNAME_FLAGGED_SUBSCRIPTION,
updateQuery: (
prev,
- { subscriptionData: { data: { usernameFlagged: user } } }
+ {
+ subscriptionData: {
+ data: { usernameFlagged: user },
+ },
+ }
) => {
return handleFlaggedAccountsChange(prev, user, () => {
const msg = t(
@@ -62,7 +66,11 @@ class FlaggedAccountsContainer extends Component {
document: USERNAME_APPROVED_SUBSCRIPTION,
updateQuery: (
prev,
- { subscriptionData: { data: { usernameApproved: user } } }
+ {
+ subscriptionData: {
+ data: { usernameApproved: user },
+ },
+ }
) => {
return handleFlaggedAccountsChange(prev, user, () => {
const msg = t(
@@ -78,7 +86,11 @@ class FlaggedAccountsContainer extends Component {
document: USERNAME_REJECTED_SUBSCRIPTION,
updateQuery: (
prev,
- { subscriptionData: { data: { usernameRejected: user } } }
+ {
+ subscriptionData: {
+ data: { usernameRejected: user },
+ },
+ }
) => {
return handleFlaggedAccountsChange(prev, user, () => {
const msg = t(
@@ -96,7 +108,9 @@ class FlaggedAccountsContainer extends Component {
prev,
{
subscriptionData: {
- data: { usernameChanged: { previousUsername, user } },
+ data: {
+ usernameChanged: { previousUsername, user },
+ },
},
}
) => {
@@ -297,7 +311,10 @@ const mapDispatchToProps = dispatch =>
);
export default compose(
- connect(null, mapDispatchToProps),
+ connect(
+ null,
+ mapDispatchToProps
+ ),
withApproveUsername,
withQuery(
gql`
diff --git a/client/coral-admin/src/routes/Community/containers/Indicator.js b/client/coral-admin/src/routes/Community/containers/Indicator.js
index 9e5be831a..3db93cdd0 100644
--- a/client/coral-admin/src/routes/Community/containers/Indicator.js
+++ b/client/coral-admin/src/routes/Community/containers/Indicator.js
@@ -15,7 +15,11 @@ class IndicatorContainer extends Component {
document: USERNAME_FLAGGED_SUBSCRIPTION,
updateQuery: (
prev,
- { subscriptionData: { data: { usernameFlagged: user } } }
+ {
+ subscriptionData: {
+ data: { usernameFlagged: user },
+ },
+ }
) => {
return handleIndicatorChange(prev, user);
},
@@ -24,7 +28,11 @@ class IndicatorContainer extends Component {
document: USERNAME_APPROVED_SUBSCRIPTION,
updateQuery: (
prev,
- { subscriptionData: { data: { usernameApproved: user } } }
+ {
+ subscriptionData: {
+ data: { usernameApproved: user },
+ },
+ }
) => {
return handleIndicatorChange(prev, user);
},
@@ -33,7 +41,11 @@ class IndicatorContainer extends Component {
document: USERNAME_REJECTED_SUBSCRIPTION,
updateQuery: (
prev,
- { subscriptionData: { data: { usernameRejected: user } } }
+ {
+ subscriptionData: {
+ data: { usernameRejected: user },
+ },
+ }
) => {
return handleIndicatorChange(prev, user);
},
@@ -42,7 +54,13 @@ class IndicatorContainer extends Component {
document: USERNAME_CHANGED_SUBSCRIPTION,
updateQuery: (
prev,
- { subscriptionData: { data: { usernameChanged: { user } } } }
+ {
+ subscriptionData: {
+ data: {
+ usernameChanged: { user },
+ },
+ },
+ }
) => {
return handleIndicatorChange(prev, user);
},
@@ -98,9 +116,15 @@ const fields = `
status {
username {
status
+ history {
+ status
+ }
}
}
}
+ action_summaries {
+ count
+ }
`;
const USERNAME_FLAGGED_SUBSCRIPTION = gql`
diff --git a/client/coral-admin/src/routes/Community/containers/People.js b/client/coral-admin/src/routes/Community/containers/People.js
index f8271b236..63cd28833 100644
--- a/client/coral-admin/src/routes/Community/containers/People.js
+++ b/client/coral-admin/src/routes/Community/containers/People.js
@@ -200,7 +200,10 @@ const SEARCH_QUERY = gql`
`;
export default compose(
- connect(null, mapDispatchToProps),
+ connect(
+ null,
+ mapDispatchToProps
+ ),
withSetUserRole,
withUnsuspendUser,
withUnbanUser,
diff --git a/client/coral-admin/src/routes/Community/containers/RejectUsernameDialog.js b/client/coral-admin/src/routes/Community/containers/RejectUsernameDialog.js
index 325eadfb5..a09bf117e 100644
--- a/client/coral-admin/src/routes/Community/containers/RejectUsernameDialog.js
+++ b/client/coral-admin/src/routes/Community/containers/RejectUsernameDialog.js
@@ -19,6 +19,9 @@ const mapDispatchToProps = dispatch =>
);
export default compose(
- connect(mapStateToProps, mapDispatchToProps),
+ connect(
+ mapStateToProps,
+ mapDispatchToProps
+ ),
withRejectUsername
)(RejectUsernameDialog);
diff --git a/client/coral-admin/src/routes/Community/graphql.js b/client/coral-admin/src/routes/Community/graphql.js
index 8d6941c41..c106fdd61 100644
--- a/client/coral-admin/src/routes/Community/graphql.js
+++ b/client/coral-admin/src/routes/Community/graphql.js
@@ -106,6 +106,23 @@ export function handleFlaggedAccountsChange(root, user, notify) {
}
}
+export const wasUsernameReported = user => {
+ const previousStatus =
+ user.state.status.username.history[
+ user.state.status.username.history.length - 2
+ ];
+
+ // Check for correct previous status
+ if (!['SET', 'CHANGES'].includes(previousStatus)) {
+ return false;
+ }
+ // Check for flags
+ if (user.action_summaries.every(as => as.count === 0)) {
+ return false;
+ }
+ return true;
+};
+
/**
* Track indicator status
* @param {Object} root current state of the store
@@ -119,7 +136,9 @@ export function handleIndicatorChange(root, user) {
return incrementFlaggedUserCount(root);
case 'APPROVED':
case 'REJECTED':
- return decrementFlaggedUserCount(root);
+ if (wasUsernameReported(user)) {
+ return decrementFlaggedUserCount(root);
+ }
default:
}
}
diff --git a/client/coral-admin/src/routes/Configure/components/EmbedLink.js b/client/coral-admin/src/routes/Configure/components/EmbedLink.js
index f03716e43..4ed50e98e 100644
--- a/client/coral-admin/src/routes/Configure/components/EmbedLink.js
+++ b/client/coral-admin/src/routes/Configure/components/EmbedLink.js
@@ -34,7 +34,7 @@ class EmbedLink extends Component {
`;
return (
-
+
{t('configure.copy_and_paste')}
|