mirror of
https://github.com/wassname/talk.git
synced 2026-09-12 13:01:11 +08:00
Merge branch 'master' into user-detail
This commit is contained in:
@@ -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;
|
||||
@@ -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 {
|
||||
@@ -27,6 +35,22 @@ import { humanizeNumber } from 'coral-framework/helpers/numbers';
|
||||
|
||||
const filterOutLocalProfiles = ({ provider }) => provider !== 'local';
|
||||
|
||||
/**
|
||||
* 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);
|
||||
@@ -44,6 +68,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}>
|
||||
@@ -64,18 +94,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,
|
||||
@@ -103,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;
|
||||
@@ -111,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,
|
||||
@@ -143,7 +203,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 })}>
|
||||
@@ -170,6 +230,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>
|
||||
)}
|
||||
|
||||
@@ -375,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,
|
||||
@@ -391,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;
|
||||
|
||||
@@ -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,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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user