mirror of
https://github.com/wassname/talk.git
synced 2026-06-30 17:59:32 +08:00
Adds graphql for users flagged in the server and client.
This commit is contained in:
@@ -7,10 +7,7 @@ import {
|
||||
SORT_UPDATE,
|
||||
COMMENTERS_NEW_PAGE,
|
||||
SET_ROLE,
|
||||
SET_COMMENTER_STATUS,
|
||||
FETCH_FLAGGED_COMMENTERS_REQUEST,
|
||||
FETCH_FLAGGED_COMMENTERS_SUCCESS,
|
||||
FETCH_FLAGGED_COMMENTERS_FAILURE
|
||||
SET_COMMENTER_STATUS
|
||||
} from '../constants/community';
|
||||
|
||||
import coralApi from '../../../coral-framework/helpers/response';
|
||||
@@ -18,7 +15,7 @@ import coralApi from '../../../coral-framework/helpers/response';
|
||||
export const fetchAccounts = (query = {}) => dispatch => {
|
||||
dispatch(requestFetchAccounts());
|
||||
coralApi(`/users?${qs.stringify(query)}`)
|
||||
.then(({result, page, count, limit, totalPages}) =>
|
||||
.then(({result, page, count, limit, totalPages}) =>{
|
||||
dispatch({
|
||||
type: FETCH_COMMENTERS_SUCCESS,
|
||||
accounts: result,
|
||||
@@ -26,8 +23,8 @@ export const fetchAccounts = (query = {}) => dispatch => {
|
||||
count,
|
||||
limit,
|
||||
totalPages
|
||||
})
|
||||
)
|
||||
});
|
||||
})
|
||||
.catch(error => dispatch({type: FETCH_COMMENTERS_FAILURE, error}));
|
||||
};
|
||||
|
||||
@@ -58,25 +55,3 @@ export const setCommenterStatus = (id, status) => (dispatch) => {
|
||||
return dispatch({type: SET_COMMENTER_STATUS, id, status});
|
||||
});
|
||||
};
|
||||
|
||||
// Fetch flagged accounts to display in the moderation queue of the community.
|
||||
|
||||
export const fetchFlaggedAccounts = (query = {}) => dispatch => {
|
||||
dispatch(requestFetchFlaggedAccounts());
|
||||
coralApi(`/users?${qs.stringify(query)}`)
|
||||
.then(({result, page, count, limit, totalPages}) =>
|
||||
dispatch({
|
||||
type: FETCH_FLAGGED_COMMENTERS_SUCCESS,
|
||||
flaggedAccounts: result,
|
||||
page,
|
||||
count,
|
||||
limit,
|
||||
totalPages
|
||||
})
|
||||
)
|
||||
.catch(error => dispatch({type: FETCH_FLAGGED_COMMENTERS_FAILURE, error}));
|
||||
};
|
||||
|
||||
const requestFetchFlaggedAccounts = () => ({
|
||||
type: FETCH_FLAGGED_COMMENTERS_REQUEST
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
fetchAccounts,
|
||||
updateSorting,
|
||||
newPage,
|
||||
fetchFlaggedAccounts,
|
||||
} from '../../actions/community';
|
||||
|
||||
import CommunityMenu from './components/CommunityMenu';
|
||||
@@ -12,11 +13,16 @@ import People from './People';
|
||||
import FlaggedAccounts from './FlaggedAccounts';
|
||||
|
||||
class CommunityContainer extends Component {
|
||||
|
||||
// static propTypes = {
|
||||
//
|
||||
// // list of actions (approve, reject, ban) associated with the users
|
||||
// modActions: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
// }
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
console.log('DEBUG CONSTRUCTOR CommunityContainer ', props);
|
||||
|
||||
this.state = {
|
||||
searchValue: ''
|
||||
};
|
||||
@@ -49,6 +55,11 @@ class CommunityContainer extends Component {
|
||||
asc: community.ascPeople,
|
||||
...query
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.dispatch(fetchFlaggedAccounts());
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -88,8 +99,6 @@ class CommunityContainer extends Component {
|
||||
commenters={community.flaggedAccounts}
|
||||
isFetching={community.isFetchingFlagged}
|
||||
error={community.errorFlagged}
|
||||
totalPages={community.totalPagesFlagged}
|
||||
page={community.pageFlagged}
|
||||
{...this}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -6,18 +6,28 @@ const lang = new I18n(translations);
|
||||
|
||||
import styles from './Community.css';
|
||||
|
||||
// import Loading from './Loading';
|
||||
import Loading from './Loading';
|
||||
import EmptyCard from '../../components/EmptyCard';
|
||||
import User from './components/User';
|
||||
|
||||
// actions={commenter.actions}
|
||||
|
||||
const FlaggedAccounts = ({...props}) => {
|
||||
const {commenters, isFetching, error, totalPages, page} = props;
|
||||
const hasResults = !isFetching && !!commenters.length;
|
||||
const {commenters, isFetching} = props;
|
||||
const hasResults = !isFetching && commenters && !!commenters.length;
|
||||
|
||||
console.log('debug props', props);
|
||||
console.log('debug commenters', commenters);
|
||||
console.log('debug error', error);
|
||||
console.log('debug totalPages', totalPages);
|
||||
console.log('debug page', page);
|
||||
// const menuOptions = {
|
||||
// 'reject': {status: 'REJECTED', icon: 'close', key: 'r'},
|
||||
// 'approve': {status: 'ACCEPTED', icon: 'done', key: 't'},
|
||||
// 'ban': {status: 'BANNED', icon: 'not interested'}
|
||||
// };
|
||||
//
|
||||
//
|
||||
// onClickAction={this.onClickAction}
|
||||
// onClickShowBanDialog={this.onClickShowBanDialog}
|
||||
// acceptCommenter={props.acceptCommenter}
|
||||
// rejectCommenter={props.rejectCommenter}
|
||||
// menuOptions={menuOptions}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
@@ -25,7 +35,12 @@ const FlaggedAccounts = ({...props}) => {
|
||||
{ isFetching && <Loading /> }
|
||||
{
|
||||
hasResults
|
||||
? <div></div>
|
||||
? commenters.map((commenter, index) => {
|
||||
return <User
|
||||
user={commenter}
|
||||
key={index}
|
||||
index={index} />;
|
||||
})
|
||||
: <EmptyCard>{lang.t('community.no-flagged-accounts')}</EmptyCard>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.list {
|
||||
padding: 8px 0;
|
||||
list-style: none;
|
||||
display: block;
|
||||
|
||||
&.singleView .listItem {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.singleView .listItem.activeItem {
|
||||
display: block;
|
||||
height: 100%;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.5em;
|
||||
border: none;
|
||||
|
||||
.actions {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 25%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 50%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listItem {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
min-width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 14px;
|
||||
position: relative;
|
||||
transition: box-shadow 200ms;
|
||||
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sideActions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
padding: 40px 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: block;
|
||||
|
||||
.author {
|
||||
font-size: 24px;
|
||||
min-width: 50px;
|
||||
align-items: left;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.itemBody {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.created {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 20px;
|
||||
flex: 1;
|
||||
font-size: 0.88em;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.flagged {
|
||||
color: rgba(255, 0, 0, .5);
|
||||
padding-top: 15px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.flagCount{
|
||||
font-size: 12px;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: #444;
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@media (--big-viewport) {
|
||||
.listItem {
|
||||
border: 1px solid #e0e0e0;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
&.activeItem {
|
||||
border: 2px solid #333;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.hasLinks {
|
||||
color: #f00;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.banned {
|
||||
color: #f00;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.ban {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.banButton {
|
||||
width: 114px;
|
||||
letter-spacing: 1px;
|
||||
|
||||
i {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.flaggedByCount {
|
||||
display: block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.flaggedBy {
|
||||
display: inline;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.flaggedByLabel {
|
||||
font-weight: bold;
|
||||
}
|
||||
+34
-19
@@ -1,15 +1,17 @@
|
||||
import React from 'react';
|
||||
import styles from './ModerationList.css';
|
||||
import styles from '../UserModerationList.css';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
import translations from '../../../translations.json';
|
||||
|
||||
import {Icon} from 'react-mdl';
|
||||
import ActionButton from './ActionButton';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
// Render a single comment for the list
|
||||
// import {Icon} from 'react-mdl';
|
||||
// import ActionButton from './ActionButton';
|
||||
|
||||
// Render a single user for the list
|
||||
const User = props => {
|
||||
const {action, user} = props;
|
||||
const {user} = props;
|
||||
let userStatus = user.status;
|
||||
|
||||
// Do not display unless the user status is 'pending' or 'banned'.
|
||||
@@ -19,32 +21,45 @@ const User = props => {
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
<span>{user.username}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.itemBody}>
|
||||
<div className={styles.flaggedByCount}>
|
||||
<i className="material-icons">flag</i><span className={styles.flaggedByLabel}>Flags({ user.actions.length })</span>:
|
||||
{ user.action_summaries.map(
|
||||
(action, i ) => {
|
||||
return <span className={styles.flaggedBy} key={i}>
|
||||
{lang.t(`community.${action.reason}`)} ({action.count})
|
||||
</span>;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.flaggedReasons}>
|
||||
{user.actions.map(
|
||||
(action, i) => {
|
||||
return <span key={i}>
|
||||
{action.reason}
|
||||
{/* action.user.username */}
|
||||
</span>;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.sideActions}>
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{props.modActions.map(
|
||||
{/* props.modActions.map(
|
||||
(action, i) =>
|
||||
<ActionButton
|
||||
return <ActionButton
|
||||
type={action.toUpperCase()}
|
||||
key={i}
|
||||
user={user}
|
||||
menuOptionsMap={props.menuOptionsMap}
|
||||
onClickAction={props.onClickAction}
|
||||
onClickShowBanDialog={props.onClickShowBanDialog}/>
|
||||
)}
|
||||
)*/}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{userStatus === 'banned' ?
|
||||
<span className={styles.banned}><Icon name='error_outline'/> {lang.t('comment.banned_user')}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.flagCount}>
|
||||
{`${action.count} ${action.action_type === 'flag_bio' ? lang.t('user.bio_flags') : lang.t('user.username_flags')}`}
|
||||
</div>
|
||||
</li>;
|
||||
};
|
||||
|
||||
export default User;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -2,6 +2,7 @@ import {graphql} from 'react-apollo';
|
||||
|
||||
import MOST_FLAGS from './mostFlags.graphql';
|
||||
import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
|
||||
import USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql';
|
||||
|
||||
export const mostFlags = graphql(MOST_FLAGS, {
|
||||
options: () => {
|
||||
@@ -28,3 +29,5 @@ export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const modUserFlaggedQuery = graphql(USER_FLAGGED_QUERY);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
query Users {
|
||||
usersFlagged {
|
||||
id
|
||||
username
|
||||
status
|
||||
roles
|
||||
actions{
|
||||
id
|
||||
reason
|
||||
user {
|
||||
username
|
||||
}
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
reason
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,13 @@
|
||||
"banned": "Banned",
|
||||
"banned-user": "Banned User",
|
||||
"loading": "Loading results",
|
||||
"flaggedaccounts": "Account Flags",
|
||||
"flaggedaccounts": "Flagged Usernames",
|
||||
"people": "People",
|
||||
"no-flagged-accounts": "The Account Flags queue is currently empty."
|
||||
"no-flagged-accounts": "The Account Flags queue is currently empty.",
|
||||
"This user is impersonating": "Impersonation",
|
||||
"This looks like an ad/marketing": "Spam/Ads",
|
||||
"This username is offensive": "Offensive",
|
||||
"Other": "Other"
|
||||
},
|
||||
"modqueue": {
|
||||
"likes": "likes",
|
||||
@@ -149,9 +153,13 @@
|
||||
"banned": "Suspendido",
|
||||
"banned-user": "Usuario Suspendido",
|
||||
"loading": "Cargando resultados",
|
||||
"flaggedaccounts": "Cuentas Reportadas",
|
||||
"flaggedaccounts": "Nombres de Usuario Reportados",
|
||||
"people": "Gente",
|
||||
"no-flagged-accounts": "No hay ninguna cuenta reportada."
|
||||
"no-flagged-accounts": "No hay ninguna cuenta reportada.",
|
||||
"This user is impersonating": "Suplantación",
|
||||
"This looks like an ad/marketing": "Spam/Propaganda",
|
||||
"This username is offensive": "Ofensivo",
|
||||
"Other": "Otros"
|
||||
},
|
||||
"modqueue": {
|
||||
"likes": "gustos",
|
||||
|
||||
@@ -3,11 +3,58 @@ const DataLoader = require('dataloader');
|
||||
const util = require('./util');
|
||||
|
||||
const UsersService = require('../../services/users');
|
||||
const UserModel = require('../../models/user');
|
||||
|
||||
const genUserByIDs = (context, ids) => UsersService
|
||||
.findByIdArray(ids)
|
||||
.then(util.singleJoinBy(ids, 'id'));
|
||||
|
||||
/**
|
||||
* Retrieves users based on the passed in query that is filtered by the
|
||||
* current used passed in via the context.
|
||||
* @param {Object} context graph context
|
||||
* @param {Object} query query terms to apply to the users query
|
||||
*/
|
||||
const getUsersByQuery = ({user}, {ids, limit, cursor, sort}) => {
|
||||
|
||||
let users = UserModel.find();
|
||||
|
||||
// Only administrators can search for users
|
||||
if (user == null || !user.hasRoles('ADMIN')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
users = users.find();
|
||||
|
||||
if (ids) {
|
||||
users = users.find({
|
||||
id: {
|
||||
$in: ids
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (cursor) {
|
||||
if (sort === 'REVERSE_CHRONOLOGICAL') {
|
||||
users = users.where({
|
||||
created_at: {
|
||||
$lt: cursor
|
||||
}
|
||||
});
|
||||
} else {
|
||||
users = users.where({
|
||||
created_at: {
|
||||
$gt: cursor
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return users
|
||||
.sort({created_at: sort === 'REVERSE_CHRONOLOGICAL' ? -1 : 1})
|
||||
.limit(limit);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a set of loaders based on a GraphQL context.
|
||||
* @param {Object} context the context of the GraphQL request
|
||||
@@ -15,6 +62,7 @@ const genUserByIDs = (context, ids) => UsersService
|
||||
*/
|
||||
module.exports = (context) => ({
|
||||
Users: {
|
||||
getByQuery: (query) => getUsersByQuery(context, query),
|
||||
getByID: new DataLoader((ids) => genUserByIDs(context, ids))
|
||||
}
|
||||
});
|
||||
|
||||
@@ -72,6 +72,22 @@ const RootQuery = {
|
||||
}
|
||||
|
||||
return user;
|
||||
},
|
||||
|
||||
// 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.
|
||||
usersFlagged(_, args, {user, loaders: {Users, Actions}}) {
|
||||
|
||||
if (user == null || !user.hasRoles('ADMIN')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Actions.getByTypes({action_type: 'FLAG', item_type: 'USERS'})
|
||||
.then((ids) => {
|
||||
|
||||
// Perform the query using the available resolver.
|
||||
return Users.getByQuery({ids});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+18
-2
@@ -31,10 +31,10 @@ type User {
|
||||
username: String!
|
||||
|
||||
# Action summaries against the user.
|
||||
action_summaries: [ActionSummary]
|
||||
action_summaries: [FlagActionSummary]
|
||||
|
||||
# Actions completed on the parent.
|
||||
actions: [Action]
|
||||
actions: [FlagAction]
|
||||
|
||||
# the current roles of the user.
|
||||
roles: [USER_ROLES]
|
||||
@@ -60,6 +60,19 @@ type Tag {
|
||||
created_at: Date!
|
||||
}
|
||||
|
||||
# UsersQuery allows the ability to query users by a specific methods.
|
||||
input UsersQuery {
|
||||
|
||||
# Limit the number of results to be returned.
|
||||
limit: Int = 10
|
||||
|
||||
# Skip results from the last created_at timestamp.
|
||||
cursor: Date
|
||||
|
||||
# Sort the results by created_at.
|
||||
sort: SORT_ORDER = REVERSE_CHRONOLOGICAL
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Comments
|
||||
################################################################################
|
||||
@@ -497,6 +510,9 @@ type RootQuery {
|
||||
# Metrics related to user actions are saturated into the assets returned. The
|
||||
# sort will affect if it will allow
|
||||
metrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Asset]
|
||||
|
||||
# Users returned based on a query.
|
||||
usersFlagged(query: UsersQuery): [User]
|
||||
}
|
||||
|
||||
################################################################################
|
||||
|
||||
Reference in New Issue
Block a user