@@ -63,6 +72,16 @@ const Comment = ({
)
}
+ {
+ minimal && typeof selected === 'boolean' && typeof toggleSelect === 'function' && (
+
toggleSelect(e.target.value, e.target.checked)} />
+ )
+ }
{timeago(comment.created_at || Date.now() - props.index * 60 * 1000)}
@@ -187,6 +206,7 @@ Comment.propTypes = {
showBanUserDialog: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func.isRequired,
currentUserId: PropTypes.string.isRequired,
+ toggleSelect: PropTypes.func,
comment: PropTypes.shape({
body: PropTypes.string.isRequired,
action_summaries: PropTypes.array,
diff --git a/client/coral-admin/src/routes/Moderation/components/UserDetail.css b/client/coral-admin/src/routes/Moderation/components/UserDetail.css
index 1119be8c4..d4b5f0db7 100644
--- a/client/coral-admin/src/routes/Moderation/components/UserDetail.css
+++ b/client/coral-admin/src/routes/Moderation/components/UserDetail.css
@@ -41,8 +41,11 @@
}
.commentStatuses {
- padding: 0;
+ padding: 10px 0 0 0;
+ margin: 0;
+ height: 52px;
list-style: none;
+ box-sizing: border-box;
li {
display: inline-block;
@@ -56,3 +59,24 @@
font-weight: bold;
border-bottom: 3px solid #F36451;
}
+
+.bulkActionGroup {
+ height: 52px;
+ background-color: #efefef;
+
+ i {
+ margin-right: 0;
+ }
+
+ .bulkAction {
+ display: inline-block;
+ width: 48px;
+ height: 48px;
+ transform: scale(.7);
+ min-width: 0;
+ }
+
+ .bulkAction:last-child {
+ margin-left: -10px;
+ }
+}
diff --git a/client/coral-admin/src/routes/Moderation/components/UserDetail.js b/client/coral-admin/src/routes/Moderation/components/UserDetail.js
index c461220c8..688ac75d0 100644
--- a/client/coral-admin/src/routes/Moderation/components/UserDetail.js
+++ b/client/coral-admin/src/routes/Moderation/components/UserDetail.js
@@ -16,6 +16,10 @@ export default class UserDetail extends React.Component {
showSuspendUserDialog: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
+ changeStatus: PropTypes.func.isRequired,
+ toggleSelect: PropTypes.func.isRequired,
+ bulkAccept: PropTypes.func.isRequired,
+ bulkReject: PropTypes.func.isRequired,
}
copyPermalink = () => {
@@ -28,12 +32,24 @@ export default class UserDetail extends React.Component {
}
}
- changeStatus = (tab) => {
- if (tab === 'all') {
- this.props.changeStatus('all');
- } else if (tab === 'rejected') {
- this.props.changeStatus('rejected');
- }
+ rejectThenReload = (info) => {
+ this.props.rejectComment(info).then(() => {
+ this.props.data.refetch();
+ });
+ }
+
+ acceptThenReload = (info) => {
+ this.props.acceptComment(info).then(() => {
+ this.props.data.refetch();
+ });
+ }
+
+ showAll = () => {
+ this.props.changeStatus('all');
+ }
+
+ showRejected = () => {
+ this.props.changeStatus('rejected');
}
render () {
@@ -44,13 +60,17 @@ export default class UserDetail extends React.Component {
rejectedComments,
comments: {nodes}
},
- moderation: {userDetailActiveTab: tab},
+ moderation: {
+ userDetailActiveTab: tab,
+ userDetailSelectedIds: selectedIds
+ },
bannedWords,
suspectWords,
+ toggleSelect,
+ bulkAccept,
+ bulkReject,
showBanUserDialog,
showSuspendUserDialog,
- acceptComment,
- rejectComment,
hideUserDetail
} = this.props;
const localProfile = user.profiles.find((p) => p.provider === 'local');
@@ -60,7 +80,7 @@ export default class UserDetail extends React.Component {
profile = localProfile.id;
}
- let rejectedPercent = rejectedComments / totalComments;
+ let rejectedPercent = (rejectedComments / totalComments) * 100;
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
// if totalComments is 0, you're dividing by zero, which is naughty
@@ -94,14 +114,38 @@ export default class UserDetail extends React.Component {
{`${(rejectedPercent).toFixed(1)}%`}
-
+ {
+ selectedIds.length === 0
+ ? (
+
+ )
+ : (
+
+
+
+ {`${selectedIds.length} comments selected`}
+
+ )
+ }
+
{
nodes.map((comment, i) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
+ const selected = selectedIds.indexOf(comment.id) !== -1;
return ;
diff --git a/client/coral-admin/src/routes/Moderation/components/styles.css b/client/coral-admin/src/routes/Moderation/components/styles.css
index e7bea9430..d10311f74 100644
--- a/client/coral-admin/src/routes/Moderation/components/styles.css
+++ b/client/coral-admin/src/routes/Moderation/components/styles.css
@@ -185,10 +185,6 @@ span {
padding: 0 14px;
}
- &: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;
}
@@ -291,7 +287,6 @@ span {
@media (--big-viewport) {
.listItem {
- border: 1px solid #e0e0e0;
margin-bottom: 30px;
&:last-child {
@@ -460,3 +455,15 @@ span {
position: relative;
}
}
+
+.minimal {
+ margin: 0;
+}
+
+.minimalSelection {
+ background-color: #ecf4ff;
+}
+
+.bulkSelectInput {
+ cursor: pointer;
+}
diff --git a/client/coral-admin/src/routes/Moderation/containers/UserDetail.js b/client/coral-admin/src/routes/Moderation/containers/UserDetail.js
index 9afd10efb..0e36ed51a 100644
--- a/client/coral-admin/src/routes/Moderation/containers/UserDetail.js
+++ b/client/coral-admin/src/routes/Moderation/containers/UserDetail.js
@@ -6,7 +6,12 @@ import UserDetail from '../components/UserDetail';
import withQuery from 'coral-framework/hocs/withQuery';
import {getSlotsFragments} from 'coral-framework/helpers/plugins';
import {getDefinitionName} from 'coral-framework/utils';
-import {changeUserDetailStatuses} from 'coral-admin/src/actions/moderation';
+import {
+ changeUserDetailStatuses,
+ clearUserDetailSelections,
+ toggleSelectCommentInUserDetail
+} from 'coral-admin/src/actions/moderation';
+import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import Comment from './Comment';
const commentConnectionFragment = gql`
@@ -31,12 +36,37 @@ class UserDetailContainer extends React.Component {
hideUserDetail: PropTypes.func.isRequired
}
+ // status can be 'ACCEPTED' or 'REJECTED'
+ bulkSetCommentStatus = (status) => {
+ const changes = this.props.moderation.userDetailSelectedIds.map((commentId) => {
+ return this.props.setCommentStatus({commentId, status});
+ });
+
+ Promise.all(changes).then(() => {
+ this.props.data.refetch(); // some comments may have moved out of this tab
+ this.props.clearUserDetailSelections(); // un-select everything
+ });
+ }
+
+ bulkReject = () => {
+ this.bulkSetCommentStatus('REJECTED');
+ }
+
+ bulkAccept = () => {
+ this.bulkSetCommentStatus('ACCEPTED');
+ }
+
render () {
if (!('user' in this.props.root)) {
return null;
}
- return ;
+ return ;
}
}
@@ -79,10 +109,15 @@ const mapStateToProps = (state) => ({
});
const mapDispatchToProps = (dispatch) => ({
- ...bindActionCreators({changeUserDetailStatuses}, dispatch)
+ ...bindActionCreators({
+ changeUserDetailStatuses,
+ clearUserDetailSelections,
+ toggleSelectCommentInUserDetail
+ }, dispatch)
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withUserDetailQuery,
+ withSetCommentStatus,
)(UserDetailContainer);
diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js
index 71117563d..228dd4c41 100644
--- a/client/coral-embed-stream/src/components/Comment.js
+++ b/client/coral-embed-stream/src/components/Comment.js
@@ -297,6 +297,7 @@ class Comment extends React.Component {
} = this.props;
const view = this.getVisibileReplies();
+
const hasMoreComments = comment.replies && (comment.replies.hasNextPage || comment.replies.nodes.length > view.length);
const replyCount = this.hasIgnoredReplies() ? '' : comment.replyCount;
const flagSummary = getActionSummary('FlagActionSummary', comment);
diff --git a/client/coral-embed-stream/src/reducers/embed.js b/client/coral-embed-stream/src/reducers/embed.js
index 8ccf4cbbc..06073c54e 100644
--- a/client/coral-embed-stream/src/reducers/embed.js
+++ b/client/coral-embed-stream/src/reducers/embed.js
@@ -4,6 +4,7 @@ const initialState = {
activeTab: 'stream',
previousTab: '',
refetching: false,
+ refetchRequestId: 0,
};
export default function stream(state = initialState, action) {
@@ -18,7 +19,8 @@ export default function stream(state = initialState, action) {
if (action.queryString.indexOf('query CoralEmbedStream_Embed(') >= 0) {
return {
...state,
- refetching: action.isRefetch,
+ refetching: action.isRefetch ? true : state.refetching,
+ refetchRequestId: action.isRefetch ? action.requestId : state.refetchRequestId,
};
}
return state;
@@ -26,7 +28,7 @@ export default function stream(state = initialState, action) {
if (action.operationName === 'CoralEmbedStream_Embed') {
return {
...state,
- refetching: false,
+ refetching: action.requestId === state.refetchRequestId ? false : state.refetching,
};
}
return state;
diff --git a/services/passport.js b/services/passport.js
index 03602cdb6..d42dfa295 100644
--- a/services/passport.js
+++ b/services/passport.js
@@ -40,6 +40,7 @@ const SetTokenForSafari = (req, res, token) => {
if (browser.ios || browser.safari) {
res.cookie('authorization', token, {
httpOnly: true,
+ secure: process.env.NODE_ENV === 'production',
expires: new Date(Date.now() + ms(JWT_EXPIRY))
});
}
diff --git a/services/users.js b/services/users.js
index 5c6440a0e..9bca10ea5 100644
--- a/services/users.js
+++ b/services/users.js
@@ -930,16 +930,21 @@ module.exports = class UsersService {
// Extract all the tokenUserNotFound plugins so we can integrate with other
// providers.
-const tokenUserNotFoundHooks = require('./plugins')
- .get('server', 'tokenUserNotFound')
- .map(({plugin, tokenUserNotFound}) => {
- debug(`added plugin '${plugin.name}' to tokenUserNotFound hooks`);
+let tokenUserNotFoundHooks = null;
- return tokenUserNotFound;
- });
-
-// Provide a function that
+// Provide a function that can loop over the hooks and search for a provider
+// can crack the token to a user.
const lookupUserNotFound = async (token) => {
+ if (!Array.isArray(tokenUserNotFoundHooks)) {
+ tokenUserNotFoundHooks = require('./plugins')
+ .get('server', 'tokenUserNotFound')
+ .map(({plugin, tokenUserNotFound}) => {
+ debug(`added plugin '${plugin.name}' to tokenUserNotFound hooks`);
+
+ return tokenUserNotFound;
+ });
+ }
+
for (let hook of tokenUserNotFoundHooks) {
let user = await hook(token);
if (user !== null && typeof user !== 'undefined') {