Merge pull request #1523 from coralproject/reject-username

Reject username
This commit is contained in:
Kim Gardner
2018-06-05 13:44:01 +01:00
committed by GitHub
167 changed files with 3392 additions and 2469 deletions
+5
View File
@@ -2,5 +2,10 @@
"env": {
"jest": true
},
"settings": {
"react": {
"version": "15.0"
}
},
"extends": "@coralproject/eslint-config-talk"
}
+4
View File
@@ -0,0 +1,4 @@
overrides:
- files: "bin/cli*"
options:
parser: babylon
+1 -2
View File
@@ -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) {
+4 -1
View File
@@ -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}`);
@@ -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,
});
@@ -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,
};
@@ -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;
@@ -19,7 +19,9 @@ class BanUserDialog extends React.Component {
}
handleMessageChange = e => {
const { target: { value: message } } = e;
const {
target: { value: message },
} = e;
this.setState({ message });
};
@@ -27,7 +27,9 @@ class KarmaTooltip extends React.Component {
};
render() {
const { thresholds: { unreliable } } = this.props;
const {
thresholds: { unreliable },
} = this.props;
const { menuVisible } = this.state;
return (
@@ -68,6 +70,7 @@ class KarmaTooltip extends React.Component {
className={styles.link}
href={t('user_detail.karma_docs_link')}
target="_blank"
rel="noopener noreferrer"
>
{t('user_detail.learn_more')}
</a>
@@ -0,0 +1,80 @@
.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;
}
.close {
display: block;
position: absolute;
top: 24px;
right: 20px;
}
.closeButton {
font-size: 24px;
line-height: 14px;
color: #363636;
&:hover {
color: #6b6b6b;
}
}
.legend {
padding: 0;
font-weight: bold;
}
div.radioGroup {
margin-top: 6px;
}
label.radioGroup {
&:global(.is-checked) > :global(.mdl-radio__outer-circle),
> :global(.mdl-radio__outer-circle) {
border-color: #212121;
}
> :global(.mdl-radio__inner-circle) {
background: #212121;
}
> :global(.mdl-radio__label) {
font-size: 14px;
line-height: 20px;
}
}
.messageInput {
border-radius: 3px;
width: 100%;
padding: 10px;
font-size: 14px;
box-sizing: border-box;
}
.cancel {
margin-right: 5px;
}
.perform {
min-width: 90px;
}
.buttons {
margin-top: 8px;
margin-bottom: 6px;
text-align: right;
}
@@ -0,0 +1,138 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Dialog, BareButton } from 'coral-ui';
import styles from './RejectUsernameDialog.css';
import cn from 'classnames';
import { RadioGroup, Radio } from 'react-mdl';
import Button from 'coral-ui/components/Button';
import { username as flagReason } from 'coral-framework/graphql/flagReasons';
import t from 'coral-framework/services/i18n';
const initialState = { reason: flagReason.offensive, message: '' };
class RejectUsernameDialog extends React.Component {
state = initialState;
componentWillReceiveProps(next) {
if (this.props.open && !next.open) {
this.setState(initialState);
}
}
handleReasonChange = event => {
this.setState({ reason: event.target.value });
};
handleMessageChange = event => {
this.setState({ message: event.target.value });
};
handlePerform = () => {
this.props.onPerform({
reason: this.state.reason,
message: this.state.message,
});
};
render() {
const { open, onCancel } = this.props;
const { reason, message } = this.state;
return (
<Dialog
className={cn(styles.dialog, 'talk-admin-reject-username-dialog')}
id="rejectUsernameDialog"
onCancel={onCancel}
open={open}
>
<div className={styles.close}>
<BareButton
aria-label="Close"
onClick={onCancel}
className={styles.closeButton}
>
×
</BareButton>
</div>
<section className="talk-admin-reject-username-dialog-section">
<h1 className={styles.header}>
{t('reject_username_dialog.title')}: {this.props.username}
</h1>
<p className={styles.description}>
{t('reject_username_dialog.description')}
</p>
<fieldset>
<legend className={styles.legend}>
{t('reject_username_dialog.reason')}
</legend>
<RadioGroup
name="reason"
value={reason}
childContainer="div"
onChange={this.handleReasonChange}
className={styles.radioGroup}
>
<Radio value={flagReason.offensive}>
{t('flag_reasons.username.offensive')}
</Radio>
<Radio value={flagReason.nolike}>
{t('flag_reasons.username.nolike')}
</Radio>
<Radio value={flagReason.impersonating}>
{t('flag_reasons.username.impersonating')}
</Radio>
<Radio value={flagReason.spam}>
{t('flag_reasons.username.spam')}
</Radio>
<Radio value={flagReason.other}>
{t('flag_reasons.username.other')}
</Radio>
</RadioGroup>
{reason === flagReason.other && (
<fieldset>
<legend className={styles.legend}>
{t('reject_username_dialog.message')}
</legend>
<textarea
rows={5}
className={styles.messageInput}
value={message}
onChange={this.handleMessageChange}
/>
</fieldset>
)}
</fieldset>
<div className={styles.buttons}>
<Button
cStyle="white"
className={styles.cancel}
onClick={onCancel}
raised
>
{t('reject_username_dialog.cancel')}
</Button>
<Button
cStyle="black"
className={cn(
styles.perform,
'talk-admin-reject-username-dialog-continue'
)}
onClick={this.handlePerform}
raised
>
{t('reject_username_dialog.reject_username')}
</Button>
</div>
</section>
</Dialog>
);
}
}
RejectUsernameDialog.propTypes = {
open: PropTypes.bool.isRequired,
onPerform: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
username: PropTypes.string,
};
export default RejectUsernameDialog;
+95 -11
View File
@@ -6,7 +6,15 @@ import styles from './UserDetail.css';
import UserHistory from './UserHistory';
import { Slot } from 'coral-framework/components';
import UserDetailCommentList from '../components/UserDetailCommentList';
import { isSuspended, isBanned, getKarma } from 'coral-framework/utils/user';
import {
isSuspended,
isUsernameRejected,
isUsernameChanged,
isBanned,
getKarma,
} from 'coral-framework/utils/user';
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
import ClickOutside from 'coral-framework/components/ClickOutside';
import {
@@ -25,6 +33,22 @@ import KarmaTooltip from './KarmaTooltip';
import t from 'coral-framework/services/i18n';
import { humanizeNumber } from 'coral-framework/helpers/numbers';
/**
* getUserStatusArray
* returns an array of active status(es)
* i.e if suspension is active, it returns suspension
*/
function getUserStatusArray(user) {
const statusMap = {
suspended: isSuspended,
banned: isBanned,
usernameRejected: isUsernameRejected,
usernameChanged: isUsernameChanged,
};
return Object.keys(statusMap).filter(k => statusMap[k](user));
}
class UserDetail extends React.Component {
changeTab = tab => {
this.props.changeTab(tab);
@@ -42,6 +66,12 @@ class UserDetail extends React.Component {
username: this.props.root.user.username,
});
showRejectUsernameDialog = () =>
this.props.showRejectUsernameDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
renderLoading() {
return (
<ClickOutside onClickOutside={this.props.hideUserDetail}>
@@ -62,18 +92,46 @@ 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 (
<span>
{t('user_detail.username')}
{` `}
<Icon name="cancel" />
</span>
);
case 'usernameChanged':
return (
<span>
{t('user_detail.username')}
{` `}
<Icon name="access_time" />
</span>
);
default:
return activeStatus;
}
}
return '';
}
goToReportedUsernames = () => {
const { router } = this.props;
router.push('/admin/community/flagged');
};
renderLoaded() {
const {
root,
@@ -101,7 +159,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;
@@ -109,6 +167,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,
@@ -141,7 +201,7 @@ class UserDetail extends React.Component {
},
'talk-admin-user-detail-actions-button'
)}
label={this.getActionMenuLabel()}
label={this.getActionMenuLabel(user)}
>
{suspended ? (
<ActionsMenuItem onClick={() => unsuspendUser({ id: user.id })}>
@@ -168,6 +228,27 @@ class UserDetail extends React.Component {
{t('user_detail.ban')}
</ActionsMenuItem>
)}
{usernameChanged && (
<ActionsMenuItem onClick={this.goToReportedUsernames}>
{t('user_detail.username_needs_approval')}
{` `}
<Icon name="launch" />
</ActionsMenuItem>
)}
{usernameRejected && !usernameChanged ? (
<ActionsMenuItem disabled>
{t('user_detail.username_rejected')}
</ActionsMenuItem>
) : (
<ActionsMenuItem
onClick={this.showRejectUsernameDialog}
disabled={me.id === user.id || usernameChanged}
>
{t('user_detail.reject_username')}
</ActionsMenuItem>
)}
</ActionsMenu>
)}
@@ -359,6 +440,7 @@ class UserDetail extends React.Component {
}
UserDetail.propTypes = {
router: PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
hideUserDetail: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
@@ -375,11 +457,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;
@@ -114,6 +114,7 @@ class UserDetailComment extends React.Component {
className={styles.external}
href={`${comment.asset.url}?commentId=${comment.id}`}
target="_blank"
rel="noopener noreferrer"
>
<Icon name="open_in_new" /> {t('comment.view_context')}
</a>
@@ -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,
@@ -0,0 +1,2 @@
export const SHOW_REJECT_USERNAME_DIALOG = 'SHOW_REJECT_USERNAME_DIALOG';
export const HIDE_REJECT_USERNAME_DIALOG = 'HIDE_REJECT_USERNAME_DIALOG';
@@ -77,7 +77,10 @@ const mapDispatchToProps = dispatch => ({
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
withBanUser,
withSetCommentStatus
)(BanUserDialogContainer);
+6 -1
View File
@@ -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 {
>
<BanUserDialog />
<SuspendUserDialog />
<RejectUsernameDialog />
<UserDetail />
{children}
</Layout>
@@ -79,4 +81,7 @@ const mapDispatchToProps = dispatch =>
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
export default connect(
mapStateToProps,
mapDispatchToProps
)(LayoutContainer);
@@ -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 (
<RejectUsernameDialog
open={this.props.open}
onPerform={this.rejectUsername}
onCancel={this.props.hideRejectUsernameDialog}
username={this.props.username}
/>
);
}
}
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);
+4 -1
View File
@@ -55,4 +55,7 @@ SignInContainer.propTypes = {
requireRecaptcha: PropTypes.bool.isRequired,
};
export default compose(withSignIn, withPopupAuthHandler)(SignInContainer);
export default compose(
withSignIn,
withPopupAuthHandler
)(SignInContainer);
@@ -86,7 +86,10 @@ const mapDispatchToProps = dispatch => ({
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
withSuspendUser,
withSetCommentStatus,
withOrganizationName
@@ -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,
};
@@ -275,6 +281,7 @@ const mapDispatchToProps = dispatch => ({
{
showBanUserDialog,
showSuspendUserDialog,
showRejectUsernameDialog,
changeTab,
clearUserDetailSelections,
toggleSelectCommentInUserDetail,
@@ -287,9 +294,15 @@ const mapDispatchToProps = dispatch => ({
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
withUserDetailQuery,
withSetCommentStatus,
withUnbanUser,
withUnsuspendUser
withUnsuspendUser,
withRejectUsername,
withPostFlag,
withRouter
)(UserDetailContainer);
+32 -62
View File
@@ -66,7 +66,11 @@ export default {
});
},
}),
SuspendUser: ({ variables: { input: { id, until } } }) => ({
SuspendUser: ({
variables: {
input: { id, until },
},
}) => ({
update: proxy => {
const fragmentId = `User_${id}`;
@@ -92,7 +96,11 @@ export default {
});
},
}),
UnsuspendUser: ({ variables: { input: { id } } }) => ({
UnsuspendUser: ({
variables: {
input: { id },
},
}) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
@@ -117,7 +125,11 @@ export default {
});
},
}),
BanUser: ({ variables: { input: { id } } }) => ({
BanUser: ({
variables: {
input: { id },
},
}) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
@@ -142,7 +154,11 @@ export default {
});
},
}),
UnbanUser: ({ variables: { input: { id } } }) => ({
UnbanUser: ({
variables: {
input: { id },
},
}) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
@@ -194,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 },
};
@@ -214,35 +236,6 @@ export default {
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: {
@@ -254,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 },
};
@@ -274,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: {
+2
View File
@@ -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,
@@ -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;
}
}
@@ -71,7 +71,7 @@ class RejectUsernameDialog extends Component {
<Dialog
className={cn(
styles.suspendDialog,
'talk-admin-reject-username-dialog'
'talk-admin-reject-reported-username-dialog'
)}
id="rejectUsernameDialog"
open={open}
@@ -85,7 +85,7 @@ class RejectUsernameDialog extends Component {
<div
className={cn(
styles.container,
`talk-admin-reject-username-dialog-step-${stage}`
`talk-admin-reject-reported-username-dialog-step-${stage}`
)}
>
<div className={styles.description}>
@@ -101,7 +101,7 @@ class RejectUsernameDialog extends Component {
<div className={styles.emailContainer}>
<textarea
rows={5}
className={cn(styles.emailInput, 'talk-admin-reject-username-dialog-suspension-message')}
className={cn(styles.emailInput, 'talk-admin-reject-reported-username-dialog-suspension-message')}
value={this.state.email}
onChange={this.onEmailChange}/>
</div>
@@ -110,7 +110,7 @@ class RejectUsernameDialog extends Component {
<div
className={cn(
styles.modalButtons,
'talk-admin-reject-username-dialog-buttons'
'talk-admin-reject-reported-username-dialog-buttons'
)}
>
{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)}
>
@@ -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`
@@ -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`
@@ -200,7 +200,10 @@ const SEARCH_QUERY = gql`
`;
export default compose(
connect(null, mapDispatchToProps),
connect(
null,
mapDispatchToProps
),
withSetUserRole,
withUnsuspendUser,
withUnbanUser,
@@ -19,6 +19,9 @@ const mapDispatchToProps = dispatch =>
);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
withRejectUsername
)(RejectUsernameDialog);
@@ -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:
}
}
@@ -174,7 +174,10 @@ const mapDispatchToProps = dispatch =>
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
withUpdateSettings,
withConfigureQuery,
withMergedSettings('root.settings', 'pending', 'mergedSettings')
@@ -42,7 +42,10 @@ export default compose(
}
`,
}),
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
slotPassthrough: {
root,
@@ -37,7 +37,10 @@ export default compose(
}
`,
}),
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
slotPassthrough: {
root,
@@ -45,7 +45,10 @@ export default compose(
}
`,
}),
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
slotPassthrough: {
root,
@@ -39,7 +39,10 @@ export default compose(
}
`,
}),
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
slotPassthrough: {
root,
@@ -83,4 +83,7 @@ const mapDispatchToProps = dispatch =>
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(InstallContainer);
export default connect(
mapStateToProps,
mapDispatchToProps
)(InstallContainer);
@@ -170,6 +170,7 @@ class Comment extends React.Component {
className={styles.external}
href={`${comment.asset.url}?commentId=${comment.id}`}
target="_blank"
rel="noopener noreferrer"
>
<Icon name="open_in_new" /> {t('comment.view_context')}
</a>
@@ -5,7 +5,10 @@ import { Button, Spinner, Icon } from 'coral-ui';
import Story from './Story';
const StorySearch = props => {
const { root: { assets }, data: { loading } } = props;
const {
root: { assets },
data: { loading },
} = props;
if (!props.moderation.storySearchVisible) {
return null;
@@ -22,7 +22,11 @@ class IndicatorContainer extends Component {
document: COMMENT_ADDED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { commentAdded: comment } } }
{
subscriptionData: {
data: { commentAdded: comment },
},
}
) => {
return this.handleCommentChange(prev, comment);
},
@@ -31,7 +35,11 @@ class IndicatorContainer extends Component {
document: COMMENT_FLAGGED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { commentFlagged: comment } } }
{
subscriptionData: {
data: { commentFlagged: comment },
},
}
) => {
return this.handleCommentChange(prev, comment);
},
@@ -40,7 +48,11 @@ class IndicatorContainer extends Component {
document: COMMENT_EDITED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { commentEdited: comment } } }
{
subscriptionData: {
data: { commentEdited: comment },
},
}
) => {
return this.handleCommentChange(prev, comment);
},
@@ -49,7 +61,11 @@ class IndicatorContainer extends Component {
document: COMMENT_ACCEPTED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { commentAccepted: comment } } }
{
subscriptionData: {
data: { commentAccepted: comment },
},
}
) => {
return this.handleCommentChange(prev, comment);
},
@@ -58,7 +74,11 @@ class IndicatorContainer extends Component {
document: COMMENT_REJECTED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { commentRejected: comment } } }
{
subscriptionData: {
data: { commentRejected: comment },
},
}
) => {
return this.handleCommentChange(prev, comment);
},
@@ -67,7 +87,11 @@ class IndicatorContainer extends Component {
document: COMMENT_RESET_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { commentReset: comment } } }
{
subscriptionData: {
data: { commentReset: comment },
},
}
) => {
return this.handleCommentChange(prev, comment);
},
@@ -71,7 +71,9 @@ class ModerationContainer extends Component {
};
get activeTab() {
const { root: { asset, settings } } = this.props;
const {
root: { asset, settings },
} = this.props;
const id = getAssetId(this.props);
const tab = getTab(this.props);
@@ -94,7 +96,11 @@ class ModerationContainer extends Component {
variables,
updateQuery: (
prev,
{ subscriptionData: { data: { commentAdded: comment } } }
{
subscriptionData: {
data: { commentAdded: comment },
},
}
) => {
return this.handleCommentChange(prev, comment);
},
@@ -104,7 +110,11 @@ class ModerationContainer extends Component {
variables,
updateQuery: (
prev,
{ subscriptionData: { data: { commentAccepted: comment } } }
{
subscriptionData: {
data: { commentAccepted: comment },
},
}
) => {
const user =
comment.status_history[comment.status_history.length - 1]
@@ -125,7 +135,11 @@ class ModerationContainer extends Component {
variables,
updateQuery: (
prev,
{ subscriptionData: { data: { commentRejected: comment } } }
{
subscriptionData: {
data: { commentRejected: comment },
},
}
) => {
const user =
comment.status_history[comment.status_history.length - 1]
@@ -146,7 +160,11 @@ class ModerationContainer extends Component {
variables,
updateQuery: (
prev,
{ subscriptionData: { data: { commentReset: comment } } }
{
subscriptionData: {
data: { commentReset: comment },
},
}
) => {
const user =
comment.status_history[comment.status_history.length - 1]
@@ -167,7 +185,11 @@ class ModerationContainer extends Component {
variables,
updateQuery: (
prev,
{ subscriptionData: { data: { commentEdited: comment } } }
{
subscriptionData: {
data: { commentEdited: comment },
},
}
) => {
return this.handleCommentChange(prev, comment);
},
@@ -177,7 +199,11 @@ class ModerationContainer extends Component {
variables,
updateQuery: (
prev,
{ subscriptionData: { data: { commentFlagged: comment } } }
{
subscriptionData: {
data: { commentFlagged: comment },
},
}
) => {
return this.handleCommentChange(prev, comment);
},
@@ -289,7 +315,11 @@ class ModerationContainer extends Component {
};
render() {
const { root, root: { asset, settings }, data } = this.props;
const {
root,
root: { asset, settings },
data,
} = this.props;
const assetId = getAssetId(this.props);
if (assetId) {
@@ -546,7 +576,10 @@ const mapDispatchToProps = dispatch => ({
export default compose(
withQueueConfig(baseQueueConfig),
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
withSetCommentStatus,
withModQueueQuery
)(ModerationContainer);
@@ -112,4 +112,7 @@ export const withAssetSearchQuery = withQuery(
}
);
export default compose(withRouter, withAssetSearchQuery)(StorySearchContainer);
export default compose(
withRouter,
withAssetSearchQuery
)(StorySearchContainer);
@@ -113,4 +113,7 @@ StoriesContainer.propTypes = {
updateAssetState: PropTypes.func.isRequired,
};
export default connect(mapStateToProps, mapDispatchToProps)(StoriesContainer);
export default connect(
mapStateToProps,
mapDispatchToProps
)(StoriesContainer);
@@ -41,7 +41,13 @@ class EmbedContainer extends React.Component {
document: USER_BANNED_SUBSCRIPTION,
updateQuery: (
_,
{ subscriptionData: { data: { userBanned: { state } } } }
{
subscriptionData: {
data: {
userBanned: { state },
},
},
}
) => {
notify('info', t('your_account_has_been_banned'));
props.updateStatus(state.status);
@@ -51,7 +57,13 @@ class EmbedContainer extends React.Component {
document: USER_SUSPENDED_SUBSCRIPTION,
updateQuery: (
_,
{ subscriptionData: { data: { userSuspended: { state } } } }
{
subscriptionData: {
data: {
userSuspended: { state },
},
},
}
) => {
notify('info', t('your_account_has_been_suspended'));
props.updateStatus(state.status);
@@ -61,7 +73,13 @@ class EmbedContainer extends React.Component {
document: USERNAME_REJECTED_SUBSCRIPTION,
updateQuery: (
_,
{ subscriptionData: { data: { usernameRejected: { state } } } }
{
subscriptionData: {
data: {
usernameRejected: { state },
},
},
}
) => {
notify('info', t('your_username_has_been_rejected'));
props.updateStatus(state.status);
@@ -324,7 +342,10 @@ const mapDispatchToProps = dispatch =>
export default compose(
withPopupAuthHandler,
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
branch(props => !props.checkedInitialLogin, renderComponent(Spinner)),
withEmbedQuery
)(EmbedContainer);
+24 -4
View File
@@ -126,7 +126,9 @@ export default {
},
mutations: {
PostComment: ({
variables: { input: { asset_id, body, parent_id, tags = [] } },
variables: {
input: { asset_id, body, parent_id, tags = [] },
},
state: { auth },
}) => ({
optimisticResponse: {
@@ -193,7 +195,13 @@ export default {
updateQueries: {
CoralEmbedStream_Embed: (
prev,
{ mutationResult: { data: { createComment: { comment } } } }
{
mutationResult: {
data: {
createComment: { comment },
},
},
}
) => {
if (
(![ADMIN, MODERATOR].includes(prev.me.role) &&
@@ -208,7 +216,13 @@ export default {
},
CoralEmbedStream_Profile: (
prev,
{ mutationResult: { data: { createComment: { comment } } } }
{
mutationResult: {
data: {
createComment: { comment },
},
},
}
) => {
return update(prev, {
me: {
@@ -224,7 +238,13 @@ export default {
updateQueries: {
CoralEmbedStream_Embed: (
prev,
{ mutationResult: { data: { editComment: { comment } } } }
{
mutationResult: {
data: {
editComment: { comment },
},
},
}
) => {
if (
!['PREMOD', 'REJECTED', 'SYSTEM_WITHHELD'].includes(comment.status)
@@ -139,7 +139,10 @@ const mapDispatchToProps = dispatch =>
);
const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
withSettingsFragments,
withUpdateAssetSettings,
withMergedSettings('asset.settings', 'pending', 'mergedSettings')
@@ -22,7 +22,14 @@ class CommentHistoryContainer extends Component {
limit: 5,
cursor: this.props.root.me.comments.endCursor,
},
updateQuery: (previous, { fetchMoreResult: { me: { comments } } }) => {
updateQuery: (
previous,
{
fetchMoreResult: {
me: { comments },
},
}
) => {
const updated = update(previous, {
me: {
comments: {
@@ -97,7 +104,10 @@ const mapStateToProps = state => ({
});
export default compose(
connect(mapStateToProps, null),
connect(
mapStateToProps,
null
),
withCommentHistoryFragments,
withFetchMore
)(CommentHistoryContainer);
@@ -80,6 +80,7 @@ const mapStateToProps = state => ({
currentUser: state.auth.user,
});
export default compose(connect(mapStateToProps), withProfileQuery)(
ProfileContainer
);
export default compose(
connect(mapStateToProps),
withProfileQuery
)(ProfileContainer);
@@ -55,7 +55,10 @@ export default compose(
${Settings.fragments.root}
`,
}),
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
withSlotElements({
slot: 'profileSettings',
propName: 'profileSettingsSlotElements',
@@ -117,8 +117,12 @@ export default class Comment extends React.Component {
}
componentWillReceiveProps(next) {
const { comment: { replies: prevReplies } } = this.props;
const { comment: { replies: nextReplies } } = next;
const {
comment: { replies: prevReplies },
} = this.props;
const {
comment: { replies: nextReplies },
} = next;
if (
prevReplies &&
nextReplies &&
@@ -243,7 +247,10 @@ export default class Comment extends React.Component {
}
loadNewReplies = () => {
const { comment: { replies, replyCount, id }, emit } = this.props;
const {
comment: { replies, replyCount, id },
emit,
} = this.props;
if (replyCount > replies.nodes.length) {
this.setState({ loadingState: 'loading' });
this.props
@@ -292,7 +299,11 @@ export default class Comment extends React.Component {
// getVisibileReplies returns a list containing comments
// which were authored by current user or comes before the `idCursor`.
getVisibileReplies() {
const { comment: { replies }, currentUser, liveUpdates } = this.props;
const {
comment: { replies },
currentUser,
liveUpdates,
} = this.props;
const idCursor = this.state.idCursors[0];
const userId = currentUser ? currentUser.id : null;
@@ -18,6 +18,7 @@ const ModerationLink = props =>
className="talk-embed-stream-moderation-link"
href={`${BASE_PATH}admin/moderate/${props.assetId}`}
target="_blank"
rel="noopener noreferrer"
>
{t('moderate_this_stream')}
</a>
@@ -211,7 +211,10 @@ class Stream extends React.Component {
root,
appendItemArray,
asset,
asset: { comment: highlightedComment, settings: { questionBoxEnable } },
asset: {
comment: highlightedComment,
settings: { questionBoxEnable },
},
postComment,
notify,
updateItem,
@@ -215,7 +215,10 @@ const mapStateToProps = state => ({
const enhance = compose(
withHooks(['preSubmit', 'postSubmit']),
connect(mapStateToProps, null)
connect(
mapStateToProps,
null
)
);
export default enhance(CommentBox);
@@ -33,4 +33,7 @@ const mapDispatchToProps = dispatch =>
dispatch
);
export default connect(null, mapDispatchToProps)(CommentNotFound);
export default connect(
null,
mapDispatchToProps
)(CommentNotFound);
@@ -54,7 +54,11 @@ class StreamContainer extends React.Component {
},
updateQuery: (
prev,
{ subscriptionData: { data: { commentEdited } } }
{
subscriptionData: {
data: { commentEdited },
},
}
) => {
// Ignore mutations from me.
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
@@ -87,7 +91,14 @@ class StreamContainer extends React.Component {
variables: {
assetId: this.props.asset.id,
},
updateQuery: (prev, { subscriptionData: { data: { commentAdded } } }) => {
updateQuery: (
prev,
{
subscriptionData: {
data: { commentAdded },
},
}
) => {
// Ignore mutations from me.
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
if (
@@ -488,7 +499,10 @@ const mapDispatchToProps = dispatch =>
export default compose(
withFragments(fragments),
withEmit,
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
withPostComment,
// `talk-plugin-flags` has a custom error handling logic.
withPostFlag({ notifyOnError: false }),
@@ -78,7 +78,12 @@ function markLinks(body, keyPrefix) {
matches.forEach((match, i) => {
content.push(body.substring(index, match.index));
content.push(
<a key={`${keyPrefix}_${i}`} href={match.url} target="_blank">
<a
key={`${keyPrefix}_${i}`}
href={match.url}
target="_blank"
rel="noopener noreferrer"
>
{match.text}
</a>
);
+4 -1
View File
@@ -103,5 +103,8 @@ export default compose(
size: props => props.size,
defaultComponent: props => props.defaultComponent,
}),
connect(mapStateToProps, null)
connect(
mapStateToProps,
null
)
)(Slot);
@@ -242,6 +242,18 @@ export const withUnsuspendUser = withMutation(
}
);
const SetUsernameStatusFragment = gql`
fragment Talk_SetUsernameStatus on User {
state {
status {
username {
status
}
}
}
}
`;
export const withApproveUsername = withMutation(
gql`
mutation ApproveUsername($id: ID!) {
@@ -257,6 +269,27 @@ export const withApproveUsername = withMutation(
variables: {
id,
},
update: proxy => {
const fragmentId = `User_${id}`;
const data = {
__typename: 'User',
state: {
__typename: 'UserState',
status: {
__typename: 'UserStatus',
username: {
__typename: 'UsernameStatus',
status: 'APPROVED',
},
},
},
};
proxy.writeFragment({
fragment: SetUsernameStatusFragment,
id: fragmentId,
data,
});
},
});
},
}),
@@ -278,6 +311,27 @@ export const withRejectUsername = withMutation(
variables: {
id,
},
update: proxy => {
const fragmentId = `User_${id}`;
const data = {
__typename: 'User',
state: {
__typename: 'UserState',
status: {
__typename: 'UserStatus',
username: {
__typename: 'UsernameStatus',
status: 'REJECTED',
},
},
},
};
proxy.writeFragment({
fragment: SetUsernameStatusFragment,
id: fragmentId,
data,
});
},
});
},
}),
+4 -1
View File
@@ -2,5 +2,8 @@ import { connect } from 'react-redux';
export default (mapStateToProps, ...rest) => BaseComponent => {
BaseComponent.mapStateToProps = mapStateToProps;
return connect(mapStateToProps, ...rest)(BaseComponent);
return connect(
mapStateToProps,
...rest
)(BaseComponent);
};
@@ -95,7 +95,10 @@ const mapDispatchToProps = dispatch =>
bindActionCreators({ updateUsername, updateStatus }, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
withSetUsernameMutation,
withSetUsername
);
+4 -1
View File
@@ -121,4 +121,7 @@ const withSignUp = hoistStatics(WrappedComponent => {
return WithSignUp;
});
export default compose(withSettingsQuery, withSignUp);
export default compose(
withSettingsQuery,
withSignUp
);
@@ -198,5 +198,11 @@ const mapStateToProps = state => ({
* })(MyComponent);
*/
export default settings => {
return compose(connect(mapStateToProps, null), createHOC(settings));
return compose(
connect(
mapStateToProps,
null
),
createHOC(settings)
);
};
+5 -1
View File
@@ -9,7 +9,11 @@ export function createReduxEmitter(eventEmitter) {
// Handle apollo actions.
if (action.type.startsWith('APOLLO_')) {
if (action.type === 'APOLLO_SUBSCRIPTION_RESULT') {
const { operationName, variables, result: { data } } = action;
const {
operationName,
variables,
result: { data },
} = action;
eventEmitter.emit(`subscription.${operationName}.data`, {
variables,
data,
+18
View File
@@ -35,6 +35,24 @@ export const isBanned = user => {
return get(user, 'state.status.banned.status');
};
/**
* isUsernameRejected
* retrieves boolean based on the username status
*/
export const isUsernameRejected = user => {
return get(user, 'state.status.username.status') === 'REJECTED';
};
/**
* isUsernameChanged
* retrieves boolean based on the username status
*/
export const isUsernameChanged = user => {
return get(user, 'state.status.username.status') === 'CHANGED';
};
/**
* canUsernameBeUpdated
* retrieves boolean whether a username can be updated or not
+3 -1
View File
@@ -64,7 +64,9 @@ const CONFIG = {
process.env.TALK_LOGGING_LEVEL
)
? process.env.TALK_LOGGING_LEVEL
: process.env.NODE_ENV === 'test' ? 'fatal' : 'info',
: process.env.NODE_ENV === 'test'
? 'fatal'
: 'info',
// REVISION_HASH when using the docker build will contain the build hash that
// it was built at.
+3 -1
View File
@@ -149,7 +149,9 @@ class Context {
* operations.
*/
static forSystem() {
const { models: { User } } = connectors;
const {
models: { User },
} = connectors;
// Create the system user.
const user = new User({ system: true });
+3 -1
View File
@@ -1,7 +1,9 @@
const { forEachField } = require('./utils');
const { maskErrors } = require('graphql-errors');
const { TalkError } = require('../errors');
const { Error: { ValidationError } } = require('mongoose');
const {
Error: { ValidationError },
} = require('mongoose');
// If an APIError happens in a mutation, then respond with `{errors: Array}`
// according to the schema.
+14 -3
View File
@@ -6,7 +6,11 @@ const { first, get, merge, remove, groupBy, reduce, isNil } = require('lodash');
* Gets actions based on their item id's.
*/
const genActionsByItemID = (
{ connectors: { services: { Actions } } },
{
connectors: {
services: { Actions },
},
},
item_ids
) => {
return Actions.findByItemIdArray(item_ids).then(
@@ -21,7 +25,12 @@ const genActionsByItemID = (
* @param {Array<String>} itemIDs the items that we need to get the actions for
*/
const genActionsAuthoredWithID = (
{ user = {}, connectors: { services: { Actions } } },
{
user = {},
connectors: {
services: { Actions },
},
},
itemIDs
) =>
Actions.getUserActions(user.id, itemIDs).then(
@@ -50,7 +59,9 @@ const iterateActionCounts = action_counts =>
* @param {Object} item the item that we're getting the actions for
*/
async function getUserActions(ctx, { action_counts, id }) {
const { loaders: { Actions } } = ctx;
const {
loaders: { Actions },
} = ctx;
// Get the total count for all action types.
const totalActionCount = reduce(
+19 -3
View File
@@ -2,7 +2,14 @@ const DataLoader = require('dataloader');
const { URL } = require('url');
const { singleJoinBy, SingletonResolver } = require('./util');
const genAssetsByID = ({ connectors: { models: { Asset } } }, ids) =>
const genAssetsByID = (
{
connectors: {
models: { Asset },
},
},
ids
) =>
Asset.find({
id: {
$in: ids,
@@ -10,7 +17,11 @@ const genAssetsByID = ({ connectors: { models: { Asset } } }, ids) =>
}).then(singleJoinBy(ids, 'id'));
const getAssetsByQuery = async (
{ connectors: { services: { Assets } } },
{
connectors: {
services: { Assets },
},
},
query
) => {
// If we are requesting based on a limit, ask for one more than we want.
@@ -126,7 +137,12 @@ const findOrCreateAssetByURL = async (ctx, url) => {
};
const findByUrl = async (
{ connectors: { errors, services: { Assets } } },
{
connectors: {
errors,
services: { Assets },
},
},
asset_url
) => {
// Try to validate that the url is valid. If the URL constructor throws an
+17 -3
View File
@@ -51,7 +51,11 @@ const genUserByIDs = async (ctx, ids) => {
return [];
}
const { connectors: { models: { User } } } = ctx;
const {
connectors: {
models: { User },
},
} = ctx;
return User.find({ id: { $in: ids } }).then(util.singleJoinBy(ids, 'id'));
};
@@ -63,7 +67,12 @@ const genUserByIDs = async (ctx, ids) => {
* @param {Object} query query terms to apply to the users query
*/
const getUsersByQuery = async (
{ user, connectors: { models: { User } } },
{
user,
connectors: {
models: { User },
},
},
{ limit, cursor, value = '', state, action_type, sortOrder }
) => {
let query = User.find();
@@ -175,7 +184,12 @@ const getUsersByQuery = async (
* query
*/
const getCountByQuery = async (
{ user, connectors: { models: { User } } },
{
user,
connectors: {
models: { User },
},
},
{ action_type, state }
) => {
const query = User.find();
+16 -3
View File
@@ -11,7 +11,9 @@ const { IGNORE_FLAGS_AGAINST_STAFF } = require('../../config');
* @return {Promise} resolves to the referenced item
*/
const getActionItem = async (ctx, { item_id, item_type }) => {
const { loaders: { Comments, Users } } = ctx;
const {
loaders: { Comments, Users },
} = ctx;
switch (item_type) {
case 'COMMENTS': {
@@ -42,7 +44,13 @@ const createAction = async (
ctx,
{ item_id, item_type, action_type, group_id, metadata = {} }
) => {
const { user = {}, pubsub, connectors: { services: { Actions } } } = ctx;
const {
user = {},
pubsub,
connectors: {
services: { Actions },
},
} = ctx;
// Gets the item referenced by the action.
const item = await getActionItem(ctx, { item_id, item_type });
@@ -107,7 +115,12 @@ const createAction = async (
* @return {Promise} resolves to the deleted action, or null if not found.
*/
const deleteAction = (ctx, { id }) => {
const { user, connectors: { services: { Actions } } } = ctx;
const {
user,
connectors: {
services: { Actions },
},
} = ctx;
return Actions.delete({ id, user_id: user.id });
};
+5 -1
View File
@@ -63,7 +63,11 @@ const closeNow = async (ctx, id) =>
* @param {String} id the asset's id to scrape
*/
const scrapeAsset = async (ctx, id) => {
const { connectors: { services: { Scraper } } } = ctx;
const {
connectors: {
services: { Scraper },
},
} = ctx;
return Scraper.create(ctx, id);
};
+23 -5
View File
@@ -14,7 +14,10 @@ const {
} = require('../../perms/constants');
const resolveTagsForComment = async (ctx, { asset_id, tags = [] }) => {
const { user, loaders: { Tags } } = ctx;
const {
user,
loaders: { Tags },
} = ctx;
const item_type = 'COMMENTS';
// Handle Tags
@@ -156,7 +159,11 @@ const createComment = async (
metadata = {},
}
) => {
const { user, loaders: { Comments }, pubsub } = ctx;
const {
user,
loaders: { Comments },
pubsub,
} = ctx;
// Resolve the tags for the comment.
tags = await resolveTagsForComment(ctx, { asset_id, tags });
@@ -202,7 +209,11 @@ const createComment = async (
* @return {Promise} resolves to a new comment
*/
const createPublicComment = async (ctx, comment) => {
const { connectors: { services: { Moderation } } } = ctx;
const {
connectors: {
services: { Moderation },
},
} = ctx;
// We then take the wordlist and the comment into consideration when
// considering what status to assign the new comment, and resolve the new
@@ -245,7 +256,10 @@ const createActions = async (item_id, actions = []) =>
* @param {String} status the new status of the comment
*/
const setStatus = async (ctx, { id, status }) => {
const { user, loaders: { Comments } } = ctx;
const {
user,
loaders: { Comments },
} = ctx;
let comment = await CommentsService.pushStatus(
id,
@@ -281,7 +295,11 @@ const editComment = async (
ctx,
{ id, asset_id, edit: { body, metadata = {} } }
) => {
const { connectors: { services: { Moderation } } } = ctx;
const {
connectors: {
services: { Moderation },
},
} = ctx;
// Build up the new comment we're setting. We need to check this with
// moderation now.
+8 -2
View File
@@ -92,7 +92,11 @@ const actionDecrTransformer = ({ item_id, action_type, group_id }) => {
// delUser will delete a given user with the specified id.
const delUser = async (ctx, id) => {
const { connectors: { models: { User, Action, Comment } } } = ctx;
const {
connectors: {
models: { User, Action, Comment },
},
} = ctx;
// Find the user we're removing.
const user = await User.findOne({ id });
@@ -178,7 +182,9 @@ const changeUserPassword = async (ctx, oldPassword, newPassword) => {
const {
user,
loaders: { Settings },
connectors: { services: { I18n } },
connectors: {
services: { I18n },
},
} = ctx;
// Verify the old password.
+32 -5
View File
@@ -1,7 +1,13 @@
const { decorateWithTags, getRequestedFields } = require('./util');
const Asset = {
async comment({ id }, { id: commentId }, { loaders: { Comments } }) {
async comment(
{ id },
{ id: commentId },
{
loaders: { Comments },
}
) {
// Load the comment from the database.
const comment = await Comments.get.load(commentId);
if (!comment) {
@@ -15,7 +21,13 @@ const Asset = {
return comment;
},
comments({ id }, { query, deep }, { loaders: { Comments } }) {
comments(
{ id },
{ query, deep },
{
loaders: { Comments },
}
) {
if (!deep) {
query.parent_id = null;
}
@@ -25,7 +37,13 @@ const Asset = {
return Comments.getByQuery(query);
},
commentCount({ id, commentCount }, { tags }, { loaders: { Comments } }) {
commentCount(
{ id, commentCount },
{ tags },
{
loaders: { Comments },
}
) {
if (commentCount != null) {
return commentCount;
}
@@ -46,7 +64,9 @@ const Asset = {
totalCommentCount(
{ id, totalCommentCount },
{ tags },
{ loaders: { Comments } }
{
loaders: { Comments },
}
) {
if (totalCommentCount != null) {
return totalCommentCount;
@@ -64,7 +84,14 @@ const Asset = {
return Comments.countByAssetID.load(id);
},
async settings({ settings = null }, _, { loaders: { Settings } }, info) {
async settings(
{ settings = null },
_,
{
loaders: { Settings },
},
info
) {
// Get the fields we want from the settings.
const fields = getRequestedFields(info);
+49 -7
View File
@@ -16,19 +16,37 @@ const Comment = {
hasParent({ parent_id }) {
return !!parent_id;
},
parent({ parent_id }, _, { loaders: { Comments } }) {
parent(
{ parent_id },
_,
{
loaders: { Comments },
}
) {
if (parent_id == null) {
return null;
}
return Comments.get.load(parent_id);
},
user({ author_id }, _, { loaders: { Users } }) {
user(
{ author_id },
_,
{
loaders: { Users },
}
) {
if (author_id) {
return Users.getByID.load(author_id);
}
},
replies({ id, asset_id, reply_count }, { query }, { loaders: { Comments } }) {
replies(
{ id, asset_id, reply_count },
{ query },
{
loaders: { Comments },
}
) {
// Don't bother looking up replies if there aren't any there!
if (reply_count === 0) {
return {
@@ -44,17 +62,35 @@ const Comment = {
return Comments.getByQuery(query);
},
replyCount: property('reply_count'),
actions({ id }, _, { loaders: { Actions } }) {
actions(
{ id },
_,
{
loaders: { Actions },
}
) {
return Actions.getByID.load(id);
},
action_summaries(comment, _, { loaders: { Actions } }) {
action_summaries(
comment,
_,
{
loaders: { Actions },
}
) {
if (comment.action_summaries) {
return comment.action_summaries;
}
return Actions.getSummariesByItem.load(comment);
},
asset({ asset_id }, _, { loaders: { Assets } }) {
asset(
{ asset_id },
_,
{
loaders: { Assets },
}
) {
return Assets.getByID.load(asset_id);
},
editing: async (comment, _, { loaders: { Settings } }) => {
@@ -71,7 +107,13 @@ const Comment = {
editableUntil: editableUntil,
};
},
async url(comment, args, { loaders: { Assets } }) {
async url(
comment,
args,
{
loaders: { Assets },
}
) {
const asset = await Assets.getByID.load(comment.asset_id);
if (!asset) {
return null;
+64 -9
View File
@@ -6,17 +6,36 @@ const {
} = require('../../perms/constants');
const RootQuery = {
assets(_, { query }, { loaders: { Assets } }) {
assets(
_,
{ query },
{
loaders: { Assets },
}
) {
return Assets.getByQuery(query);
},
asset(_, query, { loaders: { Assets } }) {
asset(
_,
query,
{
loaders: { Assets },
}
) {
if (query.id) {
return Assets.getByID.load(query.id);
}
return Assets.getByURL(query.url);
},
settings(_, args, { loaders: { Settings } }, info) {
settings(
_,
args,
{
loaders: { Settings },
},
info
) {
// Get the fields we want from the settings.
const fields = getRequestedFields(info);
@@ -26,15 +45,33 @@ const RootQuery = {
// This endpoint is used for loading moderation queues, so hide it in the
// event that we aren't an admin.
async comments(_, { query }, { loaders: { Comments } }) {
async comments(
_,
{ query },
{
loaders: { Comments },
}
) {
return Comments.getByQuery(query);
},
comment(_, { id }, { loaders: { Comments } }) {
comment(
_,
{ id },
{
loaders: { Comments },
}
) {
return Comments.get.load(id);
},
async commentCount(_, { query }, { loaders: { Comments, Assets } }) {
async commentCount(
_,
{ query },
{
loaders: { Comments, Assets },
}
) {
const { asset_url, asset_id } = query;
if (
(!asset_id || asset_id.length === 0) &&
@@ -50,7 +87,13 @@ const RootQuery = {
return Comments.getCountByQuery(query);
},
async userCount(_, { query }, { loaders: { Users } }) {
async userCount(
_,
{ query },
{
loaders: { Users },
}
) {
return Users.getCountByQuery(query);
},
@@ -65,13 +108,25 @@ const RootQuery = {
},
// this returns an arbitrary user
user(_, { id }, { loaders: { Users } }) {
user(
_,
{ id },
{
loaders: { Users },
}
) {
return Users.getByID.load(id);
},
// This endpoint is used for loading the user moderation queues (users whose username has been flagged),
// so hide it in the event that we aren't an admin.
users(_, { query }, { loaders: { Users } }) {
users(
_,
{ query },
{
loaders: { Users },
}
) {
return Users.getByQuery(query);
},
};
+7 -1
View File
@@ -5,7 +5,13 @@ const Settings = {
karmaThresholds: (
settings,
args,
{ connectors: { services: { Karma: { THRESHOLDS } } } }
{
connectors: {
services: {
Karma: { THRESHOLDS },
},
},
}
) => THRESHOLDS,
};
+28 -4
View File
@@ -16,20 +16,44 @@ const {
const { property } = require('lodash');
const User = {
action_summaries(user, _, { loaders: { Actions } }) {
action_summaries(
user,
_,
{
loaders: { Actions },
}
) {
return Actions.getSummariesByItem.load(user);
},
actions({ id }, _, { loaders: { Actions } }) {
actions(
{ id },
_,
{
loaders: { Actions },
}
) {
return Actions.getByID.load(id);
},
comments({ id }, { query }, { loaders: { Comments } }) {
comments(
{ id },
{ query },
{
loaders: { Comments },
}
) {
// Set the author id on the query.
query.author_id = id;
return Comments.getByQuery(query);
},
ignoredUsers({ ignoresUsers }, args, { loaders: { Users } }) {
ignoredUsers(
{ ignoresUsers },
args,
{
loaders: { Users },
}
) {
// Return nothing if there is nothing to query for.
if (!ignoresUsers || ignoresUsers.length <= 0) {
return [];
+20
View File
@@ -3,6 +3,20 @@ en:
your_account_has_been_banned: Your account has been banned.
your_username_has_been_rejected: Your account has been suspended because your username has been deemed inappropriate. To restore your account please enter a new username.
embed_comments_tab: Comments
reject_username_dialog:
title: "Reject Username"
description: "Help us understand"
reason: "Reason"
message: "Reason for reporting (Optional)"
cancel: "Cancel"
reject_username: "Reject Username"
flag_reasons:
username:
offensive: "This username is offensive"
nolike: "I don't like this username"
impersonating: "This user is impersonating"
spam: "This looks like an ad/marketing"
other: "Other"
bandialog:
are_you_sure: "Are you sure you would like to ban {0}?"
ban_user: "Ban User?"
@@ -459,6 +473,12 @@ en:
user_bio: "User Bio"
username_flags: "flags for this username"
user_detail:
suspended: 'Suspended'
banned: 'Banned'
username: 'Username'
username_needs_approval: 'Username needs approval'
username_rejected: 'Username rejected'
reject_username: 'Reject Username'
remove_suspension: "Remove Suspension"
suspend: "Suspend User"
remove_ban: "Remove Ban"
+10 -5
View File
@@ -439,17 +439,22 @@ es:
user_bio: "Bio de Usuario"
username_flags: "reportes para este nombre de usuario"
user_detail:
remove_suspension: "Cancelar suspensión"
suspended: 'Suspendido'
banned: 'Baneado'
username: 'Usuario'
username_needs_approval: 'El usuario necesita aprovación'
username_rejected: 'Usuario rechazado'
suspend: "Suspender usuario"
email: "Correo electrónico"
reject_rate: "Promedio de rechazo"
all: "Todos"
rejected: "Rechazado"
remove_suspension: "Cancelar suspensión"
remove_ban: "Cancelar bloqueo"
ban: "Bloquear usuario"
member_since: "Miembro desde"
email: "Email"
total_comments: "Comentarios totales"
reject_rate: "Reject Rate"
reports: "Reportes"
all: "Todo"
rejected: "Rechazar"
user_history: "Historial del usuario"
user_history:
user_banned: "Usuario bloqueado"
+1 -1
View File
@@ -239,7 +239,7 @@
"mocha-junit-reporter": "^1.12.1",
"nightwatch": "^0.9.16",
"nodemon": "^1.11.0",
"selenium-standalone": "^6.11.0",
"selenium-standalone": "^6.15.0",
"sinon": "^3.2.1",
"sinon-chai": "^2.13.0",
"yaml-lint": "^1.0.0"
+7 -2
View File
@@ -413,7 +413,9 @@ export default (reaction, options = {}) =>
update: (
proxy,
{
data: { [`create${Reaction}Action`]: { [reaction]: action } },
data: {
[`create${Reaction}Action`]: { [reaction]: action },
},
}
) => {
const a = {
@@ -466,7 +468,10 @@ export default (reaction, options = {}) =>
${fragments.comment ? fragments.comment : ''}
`,
}),
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
withDeleteReaction,
withPostReaction
);
@@ -52,5 +52,8 @@ export default ({ sortBy = 'created_at', sortOrder = 'DESC', label }) =>
);
}
}
return connect(mapStateToProps, mapDispatchToProps)(WithSortOption);
return connect(
mapStateToProps,
mapDispatchToProps
)(WithSortOption);
});
+4 -1
View File
@@ -139,7 +139,10 @@ export default (tag, options = {}) =>
}),
withAddTag,
withRemoveTag,
connect(mapStateToProps, null)
connect(
mapStateToProps,
null
)
);
WithTags.displayName = `WithTags(${getDisplayName(WrappedComponent)})`;
+8 -1
View File
@@ -180,7 +180,14 @@ function getReactionConfig(reaction) {
[`${Reaction}Action`]: {
// This will load the user for the specific action. We'll limit this to the
// admin users only or the current logged in user.
user({ user_id }, _, { loaders: { Users }, user }) {
user(
{ user_id },
_,
{
loaders: { Users },
user,
}
) {
if (user && (user.can(SEARCH_OTHER_USERS) || user_id === user.id)) {
return Users.getByID.load(user_id);
}
@@ -5,4 +5,7 @@ import CheckSpamHook from '../components/CheckSpamHook';
const mapDispatchToProps = dispatch => bindActionCreators({ notify }, dispatch);
export default connect(null, mapDispatchToProps)(CheckSpamHook);
export default connect(
null,
mapDispatchToProps
)(CheckSpamHook);
@@ -58,6 +58,9 @@ const mapDispatchToProps = dispatch =>
);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
withForgotPassword
)(ForgotPasswordContainer);
@@ -50,4 +50,7 @@ const mapDispatchToProps = dispatch =>
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(MainContainer);
export default connect(
mapStateToProps,
mapDispatchToProps
)(MainContainer);
@@ -59,6 +59,9 @@ const mapDispatchToProps = dispatch =>
);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
withResendEmailConfirmation
)(ResendEmailConfirmatonContainer);
@@ -89,6 +89,9 @@ const mapDispatchToProps = dispatch =>
);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
withSignIn
)(SignInContainer);
@@ -145,6 +145,9 @@ const mapDispatchToProps = dispatch =>
);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
withSignUp
)(SignUpContainer);
@@ -58,7 +58,10 @@ const mapStateToProps = state => ({
});
export default compose(
connect(mapStateToProps, null),
connect(
mapStateToProps,
null
),
withSetUsername,
branch(props => !props.username, renderNothing)
)(SetUsernameDialogContainer);
@@ -11,4 +11,7 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch =>
bindActionCreators({ showSignInDialog }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(SignInButton);
export default connect(
mapStateToProps,
mapDispatchToProps
)(SignInButton);
@@ -10,4 +10,7 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => bindActionCreators({ logout }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(UserBox);
export default connect(
mapStateToProps,
mapDispatchToProps
)(UserBox);
@@ -122,7 +122,10 @@ const withAuthorNameFragments = withFragments({
});
const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
withAuthorNameFragments
);
@@ -85,7 +85,13 @@ module.exports = {
}),
resolvers: {
Comment: {
deepReplyCount({ id }, args, { loaders: { Comments } }) {
deepReplyCount(
{ id },
args,
{
loaders: { Comments },
}
) {
return Comments.getDeepCount.load(id);
},
},
@@ -6,4 +6,7 @@ import FacebookButton from '../components/FacebookButton';
const mapDispatchToProps = dispatch =>
bindActionCreators({ onClick: loginWithFacebook }, dispatch);
export default connect(null, mapDispatchToProps)(FacebookButton);
export default connect(
null,
mapDispatchToProps
)(FacebookButton);
@@ -5,7 +5,11 @@ module.exports = router => {
*/
router.get('/api/v1/auth/facebook', (req, res, next) => {
const {
connectors: { services: { Passport: { passport } } },
connectors: {
services: {
Passport: { passport },
},
},
} = req.context;
return passport.authenticate('facebook', {
@@ -22,7 +26,9 @@ module.exports = router => {
router.get('/api/v1/auth/facebook/callback', (req, res, next) => {
const {
connectors: {
services: { Passport: { passport, HandleAuthPopupCallback } },
services: {
Passport: { passport, HandleAuthPopupCallback },
},
},
} = req.context;
@@ -19,7 +19,10 @@ const mapDispatchToProps = dispatch =>
);
const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
connect(
mapStateToProps,
mapDispatchToProps
),
withTags('featured')
);
@@ -13,7 +13,10 @@ const mapDispatchToProps = dispatch =>
);
const enhance = compose(
connect(null, mapDispatchToProps),
connect(
null,
mapDispatchToProps
),
withTags('featured')
);

Some files were not shown because too many files have changed in this diff Show More