Merge branch 'master' into moderation-refactor

This commit is contained in:
Wyatt Johnson
2018-01-22 16:34:56 -07:00
committed by GitHub
45 changed files with 690 additions and 144 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ import { Router, Route, IndexRedirect, IndexRoute } from 'react-router';
import Configure from 'routes/Configure';
import Install from 'routes/Install';
import Stories from 'routes/Stories';
import Community from 'routes/Community/containers/Community';
import Community from 'routes/Community';
import { ModerationLayout, Moderation } from 'routes/Moderation';
import Layout from 'containers/Layout';
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import Layout from 'coral-admin/src/components/ui/Layout';
import Layout from 'coral-admin/src/components/Layout';
import styles from './NotFound.css';
import { Button, TextField, Alert, Success } from 'coral-ui';
import Recaptcha from 'react-recaptcha';
@@ -16,12 +16,16 @@
transform: scale(.8);
margin: 0;
&:hover {
&:not(:disabled):hover {
box-shadow: none;
color: white;
background-color: #519954;
cursor: pointer;
}
&:disabled {
cursor: not-allowed;
}
}
.active {
@@ -7,7 +7,7 @@ import { Icon } from 'coral-ui';
import t from 'coral-framework/services/i18n';
const ApproveButton = ({ active, minimal, onClick, className }) => {
const ApproveButton = ({ active, minimal, onClick, className, disabled }) => {
const text = active ? t('modqueue.approved') : t('modqueue.approve');
return (
<button
@@ -17,6 +17,7 @@ const ApproveButton = ({ active, minimal, onClick, className }) => {
className
)}
onClick={onClick}
disabled={disabled || active}
>
<Icon name={'done'} className={styles.icon} />
{!minimal && text}
@@ -28,6 +29,7 @@ ApproveButton.propTypes = {
className: PropTypes.string,
active: PropTypes.bool,
minimal: PropTypes.bool,
disabled: PropTypes.bool,
onClick: PropTypes.func,
};
@@ -7,7 +7,8 @@ import styles from './Header.css';
import t from 'coral-framework/services/i18n';
import { Logo } from './Logo';
import { can } from 'coral-framework/services/perms';
import Indicator from './Indicator';
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
import CommunityIndicator from '../routes/Community/containers/Indicator';
const CoralHeader = ({
handleLogout,
@@ -30,9 +31,7 @@ const CoralHeader = ({
activeClassName={styles.active}
>
{t('configure.moderate')}
{(root.premodCount !== 0 || root.reportedCount !== 0) && (
<Indicator />
)}
<ModerationIndicator root={root} />
</IndexLink>
)}
<Link
@@ -51,7 +50,7 @@ const CoralHeader = ({
activeClassName={styles.active}
>
{t('configure.community')}
{root.flaggedUsernamesCount !== 0 && <Indicator />}
<CommunityIndicator root={root} />
</Link>
{can(auth.user, 'UPDATE_CONFIG') && (
@@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Layout as LayoutMDL } from 'react-mdl';
import Header from '../../containers/Header';
import Drawer from '../Drawer';
import Header from '../containers/Header';
import Drawer from './Drawer';
import styles from './Layout.css';
const Layout = ({
@@ -16,12 +16,16 @@
transform: scale(.8);
margin: 0;
&:hover {
&:not(:disabled):hover {
color: white;
background-color: #D03235;
box-shadow: none;
cursor: pointer;
}
&:disabled {
cursor: not-allowed;
}
}
.active {
@@ -7,7 +7,7 @@ import { Icon } from 'coral-ui';
import t from 'coral-framework/services/i18n';
const RejectButton = ({ active, minimal, onClick, className }) => {
const RejectButton = ({ active, minimal, onClick, className, disabled }) => {
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
return (
<button
@@ -17,6 +17,7 @@ const RejectButton = ({ active, minimal, onClick, className }) => {
className
)}
onClick={onClick}
disabled={disabled || active}
>
<Icon name={'close'} className={styles.icon} />
{!minimal && text}
@@ -29,6 +30,7 @@ RejectButton.propTypes = {
active: PropTypes.bool,
minimal: PropTypes.bool,
onClick: PropTypes.func,
disabled: PropTypes.bool,
};
export default RejectButton;
+8 -12
View File
@@ -1,22 +1,18 @@
import { gql } from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import Header from '../components/ui/Header';
import Header from '../components/Header';
import CommunityIndicator from '../routes/Community/containers/Indicator';
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
import { getDefinitionName } from 'coral-framework/utils';
export default withQuery(
gql`
query TalkAdmin_Header {
__typename
premodCount: commentCount(query: { statuses: [PREMOD] })
reportedCount: commentCount(
query: { statuses: [NONE, PREMOD, SYSTEM_WITHHELD], action_type: FLAG }
)
flaggedUsernamesCount: userCount(
query: {
action_type: FLAG
state: { status: { username: [SET, CHANGED] } }
}
)
...${getDefinitionName(ModerationIndicator.fragments.root)}
...${getDefinitionName(CommunityIndicator.fragments.root)}
}
${ModerationIndicator.fragments.root}
${CommunityIndicator.fragments.root}
`,
{
options: {
+1 -1
View File
@@ -1,7 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Layout from '../components/ui/Layout';
import Layout from '../components/Layout';
import { fetchConfig } from '../actions/config';
import AdminLogin from '../components/AdminLogin';
import { FullLoading } from '../components/FullLoading';
+89 -4
View File
@@ -1,6 +1,7 @@
import update from 'immutability-helper';
import { mapLeaves } from 'coral-framework/utils';
import { gql } from 'react-apollo';
import get from 'lodash/get';
const userStatusFragment = gql`
fragment Talk_UpdateUserStatus on User {
@@ -163,9 +164,22 @@ export default {
},
}),
ApproveUsername: ({ variables: { id } }) => ({
optimisticResponse: {
approveUsername: {
__typename: 'ApproveUsernameResponse',
errors: null,
isOptimistic: true,
},
},
updateQueries: {
TalkAdmin_Community: prev => {
TalkAdmin_Community_FlaggedAccounts: (prev, { mutationResult }) => {
// Remove from list after the mutation was "really" completed.
if (get(mutationResult, 'data.approveUsername.isOptimistic')) {
return prev;
}
const updated = update(prev, {
flaggedUsernamesCount: { $apply: count => count - 1 },
flaggedUsers: {
nodes: { $apply: nodes => nodes.filter(node => node.id !== id) },
},
@@ -173,20 +187,91 @@ 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: userId } }) => ({
RejectUsername: ({ variables: { id } }) => ({
optimisticResponse: {
rejectUsername: {
__typename: 'RejectUsernameResponse',
errors: null,
isOptimistic: true,
},
},
updateQueries: {
TalkAdmin_Community: prev => {
TalkAdmin_Community_FlaggedAccounts: (prev, { mutationResult }) => {
// Remove from list after the mutation was "really" completed.
if (get(mutationResult, 'data.rejectUsername.isOptimistic')) {
return prev;
}
const updated = update(prev, {
flaggedUsernamesCount: { $apply: count => count - 1 },
flaggedUsers: {
nodes: {
$apply: nodes => nodes.filter(node => node.id !== userId),
$apply: nodes => nodes.filter(node => node.id !== id),
},
},
});
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,44 +2,31 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styles from './Community.css';
import People from '../containers/People';
import CommunityMenu from './CommunityMenu';
import RejectUsernameDialog from './RejectUsernameDialog';
import CommunityMenu from '../containers/CommunityMenu';
import RejectUsernameDialog from '../containers/RejectUsernameDialog';
import FlaggedAccounts from '../containers/FlaggedAccounts';
class Community extends Component {
renderTab() {
const { route, community, ...props } = this.props;
const { route } = this.props;
const activeTab = route.path === ':id' ? 'flagged' : route.path;
if (activeTab === 'people') {
return (
<People
community={community}
data={this.props.data}
root={this.props.root}
/>
);
return <People />;
}
return (
<div>
<FlaggedAccounts data={this.props.data} root={this.props.root} />
<RejectUsernameDialog
user={community.user}
open={community.rejectUsernameDialog}
rejectUsername={props.rejectUsername}
handleClose={props.hideRejectUsernameDialog}
/>
<FlaggedAccounts />
<RejectUsernameDialog />
</div>
);
}
render() {
const { root: { flaggedUsernamesCount } } = this.props;
return (
<div className="talk-admin-community">
<CommunityMenu flaggedUsernamesCount={flaggedUsernamesCount} />
<CommunityMenu />
<div className={styles.container}>{this.renderTab()}</div>
</div>
);
@@ -48,11 +35,6 @@ class Community extends Component {
Community.propTypes = {
route: PropTypes.object,
community: PropTypes.object,
rejectUsername: PropTypes.func.isRequired,
hideRejectUsernameDialog: PropTypes.func.isRequired,
data: PropTypes.object,
root: PropTypes.object,
};
export default Community;
@@ -17,6 +17,7 @@ class FlaggedAccounts extends React.Component {
approveUser,
me,
viewUserDetail,
hasMore,
} = this.props;
const hasResults = users.nodes && !!users.nodes.length;
@@ -60,7 +61,7 @@ class FlaggedAccounts extends React.Component {
) : (
<EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
)}
<LoadMore loadMore={loadMore} showLoadMore={users.hasNextPage} />
<LoadMore loadMore={loadMore} showLoadMore={hasMore} />
</div>
</div>
);
@@ -70,6 +71,7 @@ class FlaggedAccounts extends React.Component {
FlaggedAccounts.propTypes = {
users: PropTypes.object,
loadMore: PropTypes.func,
hasMore: PropTypes.bool,
showRejectUsernameDialog: PropTypes.func,
approveUser: PropTypes.func,
me: PropTypes.object,
@@ -9,7 +9,7 @@
min-width: 400px;
margin: 0 auto;
position: relative;
transition: box-shadow 200ms, margin-bottom 200ms;
transition: background 200ms, box-shadow 200ms, margin-bottom 200ms;
padding: 10px 0 0;
min-height: 220px;
@@ -18,6 +18,10 @@
}
}
.dangling {
background-color: #efefef;
}
.rootSelected {
composes: mdl-shadow--16dp from global;
}
@@ -6,6 +6,7 @@ import t from 'coral-framework/services/i18n';
import { username } from 'talk-plugin-flags/helpers/flagReasons';
import ApproveButton from 'coral-admin/src/components/ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import { isFlaggedUserDangling } from '../utils';
const shortReasons = {
[username.other]: t('community.other'),
@@ -28,12 +29,14 @@ class User extends React.Component {
render() {
const { user, viewUserDetail, selected, className } = this.props;
const dangling = isFlaggedUserDangling(user);
return (
<li
tabIndex={0}
className={cn(className, styles.root, {
[styles.rootSelected]: selected,
[styles.dangling]: dangling,
})}
>
<div
@@ -110,11 +113,15 @@ class User extends React.Component {
<div className={styles.actions}>
<ApproveButton
className="talk-admin-flagged-user-approve-button"
active={user.state.status.username.status === 'APPROVED'}
onClick={this.approveUser}
disabled={dangling}
/>
<RejectButton
className="talk-admin-flagged-user-reject-button"
active={user.state.status.username.status === 'REJECTED'}
onClick={this.showRejectUsernameDialog}
disabled={dangling}
/>
</div>
</div>
@@ -94,7 +94,7 @@ class RejectUsernameDialog extends Component {
{/* {
// Suspension Message: This functionality it's not entirely done on the BE - It will be released soon.
// Suspension Message: This functionality it's not entirely done on the BE - It will be released soon.
stage === 1 &&
<div className={styles.writeContainer}>
<div className={styles.emailMessage}>{t('reject_username.write_message')}</div>
@@ -1,62 +0,0 @@
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import { getDefinitionName } from 'coral-framework/utils';
import { withRejectUsername } from 'coral-framework/graphql/mutations';
import FlaggedAccounts from '../containers/FlaggedAccounts';
import FlaggedUser from '../containers/FlaggedUser';
import People from '../containers/People';
import { hideRejectUsernameDialog } from '../../../actions/community';
import Community from '../components/Community';
const mapStateToProps = state => ({
community: state.community,
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
hideRejectUsernameDialog,
},
dispatch
);
const withData = withQuery(
gql`
query TalkAdmin_Community {
flaggedUsernamesCount: userCount(
query:{
action_type: FLAG,
state: {
status: {
username: [SET, CHANGED]
}
}
}
)
...${getDefinitionName(FlaggedAccounts.fragments.root)}
...${getDefinitionName(FlaggedUser.fragments.root)}
...${getDefinitionName(People.fragments.root)}
me {
...${getDefinitionName(FlaggedUser.fragments.me)}
__typename
}
}
${People.fragments.root}
${FlaggedAccounts.fragments.root}
${FlaggedUser.fragments.root}
${FlaggedUser.fragments.me}
`,
{
options: {
fetchPolicy: 'network-only',
},
}
);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withRejectUsername,
withData
)(Community);
@@ -0,0 +1,4 @@
import withCachedFlaggedUsernamesCount from '../hocs/withCachedFlaggedUsernamesCount';
import CommunityMenu from '../components/CommunityMenu';
export default withCachedFlaggedUsernamesCount(CommunityMenu);
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import { withFragments } from 'plugin-api/beta/client/hocs';
import withQuery from 'coral-framework/hocs/withQuery';
import { Spinner } from 'coral-ui';
import PropTypes from 'prop-types';
import { withApproveUsername } from 'coral-framework/graphql/mutations';
@@ -11,15 +11,127 @@ import { viewUserDetail } from '../../../actions/userDetail';
import { getDefinitionName } from 'coral-framework/utils';
import { appendNewNodes } from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
import { handleFlaggedUsernameChange } from '../graphql';
import { notify } from 'coral-framework/actions/notification';
import { isFlaggedUserDangling } from '../utils';
import t from 'coral-framework/services/i18n';
import { notifyOnMutationError } from 'coral-framework/hocs';
import FlaggedAccounts from '../components/FlaggedAccounts';
import FlaggedUser from '../containers/FlaggedUser';
function whoChangedTheStatus(statusObject) {
return statusObject.history[statusObject.history.length - 1].assigned_by
.username;
}
function whoFlagged(user) {
return user.actions[user.actions.length - 1].user.username;
}
class FlaggedAccountsContainer extends Component {
subscriptions = [];
constructor(props) {
super(props);
}
getCountWithoutDangling() {
return this.props.root.flaggedUsers.nodes.filter(
node => !isFlaggedUserDangling(node)
).length;
}
subscribeToUpdates() {
const parameters = [
{
document: USERNAME_FLAGGED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { usernameFlagged: user } } }
) => {
return handleFlaggedUsernameChange(prev, user, () => {
const msg = t(
'flagged_usernames.notify_flagged',
whoFlagged(user),
user.username
);
this.props.notify('info', msg);
});
},
},
{
document: USERNAME_APPROVED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { usernameApproved: user } } }
) => {
return handleFlaggedUsernameChange(prev, user, () => {
const msg = t(
'flagged_usernames.notify_approved',
whoChangedTheStatus(user.state.status.username),
user.username
);
this.props.notify('info', msg);
});
},
},
{
document: USERNAME_REJECTED_SUBSCRIPTION,
updateQuery: (
prev,
{ subscriptionData: { data: { usernameRejected: user } } }
) => {
return handleFlaggedUsernameChange(prev, user, () => {
const msg = t(
'flagged_usernames.notify_rejected',
whoChangedTheStatus(user.state.status.username),
user.username
);
this.props.notify('info', msg);
});
},
},
{
document: USERNAME_CHANGED_SUBSCRIPTION,
updateQuery: (
prev,
{
subscriptionData: {
data: { usernameChanged: { previousUsername, user } },
},
}
) => {
return handleFlaggedUsernameChange(prev, user, () => {
const msg = t(
'flagged_usernames.notify_changed',
previousUsername,
user.username
);
this.props.notify('info', msg);
});
},
},
];
this.subscriptions = parameters.map(param =>
this.props.data.subscribeToMore(param)
);
}
unsubscribe() {
this.subscriptions.forEach(unsubscribe => unsubscribe());
this.subscriptions = [];
}
componentWillMount() {
this.subscribeToUpdates();
}
componentWillUnmount() {
this.unsubscribe();
}
approveUser = ({ userId: id }) => {
return this.props.approveUsername(id);
};
@@ -68,6 +180,9 @@ class FlaggedAccountsContainer extends Component {
data={this.props.data}
root={this.props.root}
users={this.props.root.flaggedUsers}
hasMore={
this.getCountWithoutDangling() < this.props.root.flaggedUsernamesCount
}
me={this.props.root.me}
/>
);
@@ -77,6 +192,8 @@ class FlaggedAccountsContainer extends Component {
FlaggedAccountsContainer.propTypes = {
showRejectUsernameDialog: PropTypes.func,
viewUserDetail: PropTypes.func,
notify: PropTypes.func,
flaggedUsernamesCount: PropTypes.number,
approveUsername: PropTypes.func,
data: PropTypes.object,
root: PropTypes.object,
@@ -105,11 +222,72 @@ const LOAD_MORE_QUERY = gql`
${FlaggedUser.fragments.user}
`;
const historyFields = `
state {
status {
username {
history {
status
assigned_by {
id
username
}
created_at
}
}
}
}
`;
const USERNAME_FLAGGED_SUBSCRIPTION = gql`
subscription TalkAdmin_UsernameFlagged {
usernameFlagged {
...${getDefinitionName(FlaggedUser.fragments.user)}
${historyFields}
}
}
${FlaggedUser.fragments.user}
`;
const USERNAME_APPROVED_SUBSCRIPTION = gql`
subscription TalkAdmin_UsernameApproved {
usernameApproved {
...${getDefinitionName(FlaggedUser.fragments.user)}
${historyFields}
}
}
${FlaggedUser.fragments.user}
`;
const USERNAME_REJECTED_SUBSCRIPTION = gql`
subscription TalkAdmin_UsernameRejected {
usernameRejected {
...${getDefinitionName(FlaggedUser.fragments.user)}
${historyFields}
}
}
${FlaggedUser.fragments.user}
`;
const USERNAME_CHANGED_SUBSCRIPTION = gql`
subscription TalkAdmin_UsernameChanged {
usernameChanged {
previousUsername
user {
...${getDefinitionName(FlaggedUser.fragments.user)}
${historyFields}
}
}
}
${FlaggedUser.fragments.user}
`;
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
showRejectUsernameDialog,
viewUserDetail,
notify,
},
dispatch
);
@@ -117,9 +295,16 @@ const mapDispatchToProps = dispatch =>
export default compose(
connect(null, mapDispatchToProps),
withApproveUsername,
withFragments({
root: gql`
fragment TalkAdminCommunity_FlaggedAccounts_root on RootQuery {
notifyOnMutationError(['approveUsername']),
withQuery(
gql`
query TalkAdmin_Community_FlaggedAccounts {
flaggedUsernamesCount: userCount(
query: {
action_type: FLAG
state: { status: { username: [SET, CHANGED] } }
}
)
flaggedUsers: users(query:{
action_type: FLAG,
state: {
@@ -138,9 +323,16 @@ export default compose(
}
me {
id
...${getDefinitionName(FlaggedUser.fragments.me)}
}
}
${FlaggedUser.fragments.user}
${FlaggedUser.fragments.me}
`,
})
{
options: {
fetchPolicy: 'network-only',
},
}
)
)(FlaggedAccountsContainer);
@@ -4,19 +4,20 @@ import { withFragments } from 'plugin-api/beta/client/hocs';
export default withFragments({
root: gql`
fragment TalkAdminCommunity_FlaggedUser_root on RootQuery {
fragment TalkAdmin_Community_FlaggedUser_root on RootQuery {
__typename
}
`,
me: gql`
fragment TalkAdminCommunity_FlaggedUser_me on User {
fragment TalkAdmin_Community_FlaggedUser_me on User {
id
}
`,
user: gql`
fragment TalkAdminCommunity_FlaggedUser_user on User {
fragment TalkAdmin_Community_FlaggedUser_user on User {
id
username
created_at
state {
status {
username {
@@ -0,0 +1,24 @@
import { compose, gql } from 'react-apollo';
import Indicator from '../../../components/Indicator';
import { withFragments } from 'plugin-api/beta/client/hocs';
import { branch, renderNothing } from 'recompose';
const hideIfNoData = hasNoData => branch(hasNoData, renderNothing);
const enhance = compose(
withFragments({
root: gql`
fragment TalkAdmin_Community_Indicator_root on RootQuery {
flaggedUsernamesCount: userCount(
query: {
action_type: FLAG
state: { status: { username: [SET, CHANGED] } }
}
)
}
`,
}),
hideIfNoData(props => !props.root.flaggedUsernamesCount)
);
export default enhance(Indicator);
@@ -4,7 +4,6 @@ import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import People from '../components/People';
import PropTypes from 'prop-types';
import { withFragments } from 'plugin-api/beta/client/hocs';
import {
withUnbanUser,
withUnsuspendUser,
@@ -16,6 +15,7 @@ import { viewUserDetail } from '../../../actions/userDetail';
import { appendNewNodes } from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
import { Spinner } from 'coral-ui';
import withQuery from 'coral-framework/hocs/withQuery';
class PeopleContainer extends React.Component {
timer = null;
@@ -39,7 +39,7 @@ class PeopleContainer extends React.Component {
query: SEARCH_QUERY,
variables: {
value,
limit: 5,
limit: 10,
},
updateQuery: (previous, { fetchMoreResult: { users } }) => {
const updated = update(previous, {
@@ -115,7 +115,6 @@ class PeopleContainer extends React.Component {
}
PeopleContainer.propTypes = {
community: PropTypes.object,
setUserRole: PropTypes.func.isRequired,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
@@ -137,7 +136,7 @@ const mapDispatchToProps = dispatch =>
);
const LOAD_MORE_QUERY = gql`
query TalkAdminCommunity_People_LoadMoreUsers(
query TalkAdmin_Community_People_LoadMoreUsers(
$limit: Int
$cursor: Cursor
$value: String
@@ -171,7 +170,7 @@ const LOAD_MORE_QUERY = gql`
`;
const SEARCH_QUERY = gql`
query TalkAdminCommunity_People_SearchUsers($value: String, $limit: Int) {
query TalkAdmin_Community_People_SearchUsers($value: String, $limit: Int) {
users(query: { value: $value, limit: $limit }) {
hasNextPage
endCursor
@@ -205,9 +204,9 @@ export default compose(
withSetUserRole,
withUnsuspendUser,
withUnbanUser,
withFragments({
root: gql`
fragment TalkAdminCommunity_People_root on RootQuery {
withQuery(
gql`
query TalkAdmin_Community_People {
users(query: {}) {
hasNextPage
endCursor
@@ -235,5 +234,10 @@ export default compose(
}
}
`,
})
{
options: {
fetchPolicy: 'network-only',
},
}
)
)(PeopleContainer);
@@ -0,0 +1,28 @@
import RejectUsernameDialog from '../components/RejectUsernameDialog';
import { withRejectUsername } from 'coral-framework/graphql/mutations';
import { hideRejectUsernameDialog } from '../../../actions/community';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose } from 'react-apollo';
import { notify } from 'coral-framework/actions/notification';
import { notifyOnMutationError } from 'coral-framework/hocs';
const mapStateToProps = state => ({
user: state.community.user,
open: state.community.rejectUsernameDialog,
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
handleClose: hideRejectUsernameDialog,
notify,
},
dispatch
);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withRejectUsername,
notifyOnMutationError(['rejectUsername'])
)(RejectUsernameDialog);
@@ -0,0 +1,107 @@
import update from 'immutability-helper';
function shouldAddFlaggedUser(root, user) {
const isEmpty = !root.flaggedUsers.nodes.length;
if (isEmpty) {
return true;
}
if (hasFlaggedUser(root, user)) {
return false;
}
const oldest = root.flaggedUsers.nodes.reduce((cur, node) => {
const createdAt = new Date(node.created_at);
return createdAt < cur ? createdAt : cur;
}, new Date());
return new Date(user.created_at) >= oldest;
}
function hasFlaggedUser(root, user) {
return root.flaggedUsers.nodes.find(u => u.id === user.id);
}
function applyUserChanges(root, user) {
const index = root.flaggedUsers.nodes.findIndex(({ id }) => id === user.id);
if (index > -1) {
return update(root, {
flaggedUsers: {
nodes: {
[index]: { $merge: user },
},
},
});
}
return root;
}
function incrementFlaggedUserCount(root) {
return update(root, {
flaggedUsernamesCount: { $apply: count => count + 1 },
});
}
function decrementFlaggedUserCount(root) {
return update(root, {
flaggedUsernamesCount: { $apply: count => count - 1 },
});
}
/**
* Assimilate flagged user changes into current store.
* @param {Object} root current state of the store
* @param {Object} user user that was changed
* @param {function} notify callback to show notification
* @return {Object} next state of the store
*/
export function handleFlaggedUsernameChange(root, user, notify) {
if (user.state.status.username.status !== 'SET') {
// Check if change came from current user, if so ignore it.
const lastChange =
user.state.status.username.history[
user.state.status.username.history.length - 1
];
if (lastChange.assigned_by.id === root.me.id) {
return root;
}
}
if (!hasFlaggedUser(root, user)) {
switch (user.state.status.username.status) {
case 'SET':
case 'CHANGED':
root = incrementFlaggedUserCount(root);
if (!shouldAddFlaggedUser(root, user)) {
return root;
}
notify();
return update(root, {
flaggedUsers: {
nodes: { $push: [user] },
},
});
break;
case 'APPROVED':
case 'REJECTED':
return root;
default:
}
}
if (hasFlaggedUser(root, user)) {
switch (user.state.status.username.status) {
case 'SET':
case 'CHANGED':
return root;
case 'APPROVED':
case 'REJECTED':
notify();
return applyUserChanges(decrementFlaggedUserCount(root), user);
default:
}
}
}
@@ -0,0 +1,29 @@
import { compose, gql } from 'react-apollo';
import { mapProps } from 'recompose';
import withQuery from 'coral-framework/hocs/withQuery';
import get from 'lodash/get';
const withData = withQuery(
gql`
query TalkAdmin_CommunityMenu {
flaggedUsernamesCount: userCount(
query: {
action_type: FLAG
state: { status: { username: [SET, CHANGED] } }
}
)
}
`,
{
options: {
fetchPolicy: 'cache-only',
},
}
);
export default compose(
withData,
mapProps(ownProps => ({
flaggedUsernamesCount: get(ownProps, 'root.flaggedUsernamesCount'),
}))
);
@@ -0,0 +1 @@
export { default } from './components/Community';
@@ -0,0 +1,3 @@
export function isFlaggedUserDangling(user) {
return ['APPROVED', 'REJECTED'].includes(user.state.status.username.status);
}
@@ -1,7 +1,7 @@
import React, { Component } from 'react';
import styles from './Install.css';
import { Wizard, WizardNav } from 'coral-ui';
import Layout from 'coral-admin/src/components/ui/Layout';
import Layout from 'coral-admin/src/components/Layout';
import InitialStep from './Steps/InitialStep';
import AddOrganizationName from './Steps/AddOrganizationName';
@@ -0,0 +1,25 @@
import { compose, gql } from 'react-apollo';
import Indicator from '../../../components/Indicator';
import { withFragments } from 'plugin-api/beta/client/hocs';
import { branch, renderNothing } from 'recompose';
const hideIfNoData = hasNoData => branch(hasNoData, renderNothing);
const enhance = compose(
withFragments({
root: gql`
fragment TalkAdmin_Moderation_Indicator_root on RootQuery {
premodCount: commentCount(query: { statuses: [PREMOD] })
reportedCount: commentCount(
query: {
statuses: [NONE, PREMOD, SYSTEM_WITHHELD]
action_type: FLAG
}
)
}
`,
}),
hideIfNoData(props => !props.root.premodCount && !props.root.reportedCount)
);
export default enhance(Indicator);
+1
View File
@@ -6,3 +6,4 @@ export { default as withEmit } from './withEmit';
export { default as excludeIf } from './excludeIf';
export { default as connect } from './connect';
export { default as withMergedSettings } from './withMergedSettings';
export { default as notifyOnMutationError } from './notifyOnMutationError';
@@ -0,0 +1,35 @@
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose } from 'react-apollo';
import { notify } from 'coral-framework/actions/notification';
import { forEachError } from 'coral-framework/utils';
import { withProps } from 'recompose';
const notifyOnMutationError = keys =>
compose(
connect(null, dispatch =>
bindActionCreators(
{
notify,
},
dispatch
)
),
withProps(ownProps =>
keys.reduce((props, key) => {
props[key] = async (...args) => {
try {
return await ownProps[key](...args);
} catch (e) {
forEachError(e, ({ msg }) => {
ownProps.notify('error', msg);
});
throw e;
}
};
return props;
}, {})
)
);
export default notifyOnMutationError;
+14 -4
View File
@@ -73,10 +73,20 @@ const createAction = async (
metadata,
});
if (action_type === 'FLAG' && item_type === 'COMMENTS') {
// The item is a comment, and this is a flag. Push that the comment was
// flagged, don't wait for it to finish.
pubsub.publish('commentFlagged', item);
if (action_type === 'FLAG') {
switch (item_type) {
case 'COMMENTS':
// The item is a comment, and this is a flag. Push that the comment was
// flagged, don't wait for it to finish.
pubsub.publish('commentFlagged', item);
break;
case 'USERS':
// The item is a user, and this is a flag. Push that the user was
// flagged, don't wait for it to finish.
pubsub.publish('usernameFlagged', item);
break;
default:
}
}
return action;
+4 -1
View File
@@ -56,7 +56,10 @@ const stopIgnoringUser = ({ user }, userToStopIgnoring) => {
};
const changeUsername = async (ctx, id, username) => {
return UsersService.changeUsername(id, username, ctx.user.id);
const user = await UsersService.changeUsername(id, username, ctx.user.id);
const previousUsername = ctx.user.username;
ctx.pubsub.publish('usernameChanged', { previousUsername, user });
return user;
};
const setUsername = async (ctx, id, username) => {
+9
View File
@@ -23,9 +23,18 @@ const Subscription = {
userSuspended(user) {
return user;
},
usernameApproved(user) {
return user;
},
usernameRejected(user) {
return user;
},
usernameFlagged(user) {
return user;
},
usernameChanged(payload) {
return payload;
},
};
module.exports = Subscription;
+22
View File
@@ -9,6 +9,8 @@ const {
SUBSCRIBE_ALL_USER_BANNED,
SUBSCRIBE_ALL_USERNAME_REJECTED,
SUBSCRIBE_ALL_USERNAME_APPROVED,
SUBSCRIBE_ALL_USERNAME_FLAGGED,
SUBSCRIBE_ALL_USERNAME_CHANGED,
} = require('../perms/constants');
const merge = require('lodash/merge');
@@ -97,6 +99,16 @@ const setupFunctions = {
}
return !args.user_id || user.id === args.user_id;
},
usernameFlagged: (options, args, user, context) => {
if (
!context.user ||
(args.user_id !== user.id &&
!context.user.can(SUBSCRIBE_ALL_USERNAME_FLAGGED))
) {
return false;
}
return !args.user_id || user.id === args.user_id;
},
usernameRejected: (options, args, user, context) => {
if (
!context.user ||
@@ -117,6 +129,16 @@ const setupFunctions = {
}
return !args.user_id || user.id === args.user_id;
},
usernameChanged: (options, args, { user }, context) => {
if (
!context.user ||
(args.user_id !== user.id &&
!context.user.can(SUBSCRIBE_ALL_USERNAME_CHANGED))
) {
return false;
}
return !args.user_id || user.id === args.user_id;
},
};
/**
+15 -1
View File
@@ -1476,6 +1476,11 @@ type RootMutation {
stopIgnoringUser(id: ID!): StopIgnoringUserResponse
}
type UsernameChangedPayload {
previousUsername: String
user: User
}
################################################################################
## Subscriptions
################################################################################
@@ -1517,14 +1522,23 @@ type Subscription {
# users with the `ADMIN` or `MODERATOR` role.
userBanned(user_id: ID): User
# Get an update whenever a username was flagged.
# `user_id` must match id of current user except for
# users with the `ADMIN` or `MODERATOR` role.
usernameFlagged(user_id: ID): User
# Get an update whenever a username has been rejected.
# `user_id` must match id of current user except for
# users with the `ADMIN` or `MODERATOR` role.
usernameRejected(user_id: ID): User
# Gen an update whenever a username has been approved. `user_id` must match id
# Get an update whenever a username has been approved. `user_id` must match id
# of current user except for users with the `ADMIN` or `MODERATOR` role.
usernameApproved(user_id: ID): User
# Get an update whenever a username has been changed. `user_id` must match id
# of current user except for users with the `ADMIN` or `MODERATOR` role.
usernameChanged(user_id: ID): UsernameChangedPayload
}
################################################################################
+5
View File
@@ -61,6 +61,11 @@ en:
reaction: 'reaction'
reactions: 'reactions'
story: 'Story'
flagged_usernames:
notify_approved: '{0} approved username {1}'
notify_rejected: '{0} rejected username {1}'
notify_flagged: '{0} reported username {1}'
notify_changed: 'user {0} changed his username to {1}'
community:
account_creation_date: "Account Creation Date"
active: Active
+2
View File
@@ -9,4 +9,6 @@ module.exports = {
SUBSCRIBE_ALL_USER_BANNED: 'SUBSCRIBE_ALL_USER_BANNED',
SUBSCRIBE_ALL_USERNAME_REJECTED: 'SUBSCRIBE_ALL_USERNAME_REJECTED',
SUBSCRIBE_ALL_USERNAME_APPROVED: 'SUBSCRIBE_ALL_USERNAME_APPROVED',
SUBSCRIBE_ALL_USERNAME_FLAGGED: 'SUBSCRIBE_ALL_USERNAME_FLAGGED',
SUBSCRIBE_ALL_USERNAME_CHANGED: 'SUBSCRIBE_ALL_USERNAME_CHANGED',
};
+2
View File
@@ -13,6 +13,8 @@ module.exports = (user, perm) => {
case types.SUBSCRIBE_ALL_USER_BANNED:
case types.SUBSCRIBE_ALL_USERNAME_REJECTED:
case types.SUBSCRIBE_ALL_USERNAME_APPROVED:
case types.SUBSCRIBE_ALL_USERNAME_FLAGGED:
case types.SUBSCRIBE_ALL_USERNAME_CHANGED:
return check(user, ['ADMIN', 'MODERATOR']);
default:
break;