diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js
index 030ae1c0c..92d10fac9 100644
--- a/client/coral-admin/src/actions/community.js
+++ b/client/coral-admin/src/actions/community.js
@@ -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
-});
diff --git a/client/coral-admin/src/containers/Community/CommunityContainer.js b/client/coral-admin/src/containers/Community/CommunityContainer.js
index abe481677..79bfd2123 100644
--- a/client/coral-admin/src/containers/Community/CommunityContainer.js
+++ b/client/coral-admin/src/containers/Community/CommunityContainer.js
@@ -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}
/>
);
diff --git a/client/coral-admin/src/containers/Community/FlaggedAccounts.js b/client/coral-admin/src/containers/Community/FlaggedAccounts.js
index 9b218de23..28fc0e1f9 100644
--- a/client/coral-admin/src/containers/Community/FlaggedAccounts.js
+++ b/client/coral-admin/src/containers/Community/FlaggedAccounts.js
@@ -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 (
@@ -25,7 +35,12 @@ const FlaggedAccounts = ({...props}) => {
{ isFetching &&
}
{
hasResults
- ?
+ ? commenters.map((commenter, index) => {
+ return
;
+ })
:
{lang.t('community.no-flagged-accounts')}
}
diff --git a/client/coral-admin/src/containers/Community/UserModerationList.css b/client/coral-admin/src/containers/Community/UserModerationList.css
new file mode 100644
index 000000000..dc0e6b57b
--- /dev/null
+++ b/client/coral-admin/src/containers/Community/UserModerationList.css
@@ -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;
+}
diff --git a/client/coral-admin/src/components/User.js b/client/coral-admin/src/containers/Community/components/User.js
similarity index 51%
rename from client/coral-admin/src/components/User.js
rename to client/coral-admin/src/containers/Community/components/User.js
index 129524993..1f18a186f 100644
--- a/client/coral-admin/src/components/User.js
+++ b/client/coral-admin/src/containers/Community/components/User.js
@@ -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 => {
+
+
+
+ flagFlags({ user.actions.length }):
+ { user.action_summaries.map(
+ (action, i ) => {
+ return
+ {lang.t(`community.${action.reason}`)} ({action.count})
+ ;
+ }
+ )}
+
+
+ {user.actions.map(
+ (action, i) => {
+ return
+ {action.reason}
+ {/* action.user.username */}
+ ;
+ }
+ )}
+
- {props.modActions.map(
+ {/* props.modActions.map(
(action, i) =>
-
- )}
+ )*/}
-
- {userStatus === 'banned' ?
- {lang.t('comment.banned_user')} : null}
-
-
-
- {`${action.count} ${action.action_type === 'flag_bio' ? lang.t('user.bio_flags') : lang.t('user.username_flags')}`}
;
};
export default User;
-
-const lang = new I18n(translations);
diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js
index 3325249e1..81aa24410 100644
--- a/client/coral-admin/src/graphql/queries/index.js
+++ b/client/coral-admin/src/graphql/queries/index.js
@@ -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);
diff --git a/client/coral-admin/src/graphql/queries/modUserFlaggedQuery.graphql b/client/coral-admin/src/graphql/queries/modUserFlaggedQuery.graphql
new file mode 100644
index 000000000..55090d8e4
--- /dev/null
+++ b/client/coral-admin/src/graphql/queries/modUserFlaggedQuery.graphql
@@ -0,0 +1,19 @@
+query Users {
+ usersFlagged {
+ id
+ username
+ status
+ roles
+ actions{
+ id
+ reason
+ user {
+ username
+ }
+ }
+ action_summaries {
+ count
+ reason
+ }
+ }
+}
diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json
index 409c47a00..eae4543ee 100644
--- a/client/coral-admin/src/translations.json
+++ b/client/coral-admin/src/translations.json
@@ -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",
diff --git a/graph/loaders/users.js b/graph/loaders/users.js
index 90c661f71..2928e41f3 100644
--- a/graph/loaders/users.js
+++ b/graph/loaders/users.js
@@ -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))
}
});
diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js
index ced4e65cf..852bc41ad 100644
--- a/graph/resolvers/root_query.js
+++ b/graph/resolvers/root_query.js
@@ -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});
+ });
}
};
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index fbdeb4279..bd032196b 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -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]
}
################################################################################