|
- {row.username}
+
{row.profiles.map(({id}) => id)}
|
diff --git a/client/coral-admin/src/routes/Community/components/User.js b/client/coral-admin/src/routes/Community/components/User.js
index 5aea161b5..2d921a1c7 100644
--- a/client/coral-admin/src/routes/Community/components/User.js
+++ b/client/coral-admin/src/routes/Community/components/User.js
@@ -38,9 +38,7 @@ const User = (props) => {
-
- {user.username}
-
+
{props.currentUser.id !== user.id &&
{
(action, j) => {
if (action.reason === action_sum.reason) {
return
- {action.user && action.user.username}: {action.message ? action.message : 'n/a'}
+ {action.user &&
+
+ }
+ : {action.message ? action.message : 'n/a'}
;
}
return null;
diff --git a/client/coral-admin/src/routes/Community/containers/Community.js b/client/coral-admin/src/routes/Community/containers/Community.js
index ee2f874a8..a9a164ac9 100644
--- a/client/coral-admin/src/routes/Community/containers/Community.js
+++ b/client/coral-admin/src/routes/Community/containers/Community.js
@@ -17,6 +17,7 @@ import {
showRejectUsernameDialog,
hideRejectUsernameDialog
} from '../../../actions/community';
+import {viewUserDetail} from '../../../actions/userDetail';
import Community from '../components/Community';
@@ -99,6 +100,7 @@ const mapDispatchToProps = (dispatch) =>
hideRejectUsernameDialog,
updateSorting,
newPage,
+ viewUserDetail,
}, dispatch);
export default compose(
diff --git a/client/coral-admin/src/routes/Community/containers/Table.js b/client/coral-admin/src/routes/Community/containers/Table.js
index 499e0718a..8bd5395e4 100644
--- a/client/coral-admin/src/routes/Community/containers/Table.js
+++ b/client/coral-admin/src/routes/Community/containers/Table.js
@@ -4,6 +4,7 @@ import {bindActionCreators} from 'redux';
import {compose} from 'react-apollo';
import {setRole, setCommenterStatus} from '../../../actions/community';
import Table from '../components/Table';
+import {viewUserDetail} from '../../../actions/userDetail';
class TableContainer extends Component {
@@ -29,6 +30,7 @@ const mapDispatchToProps = (dispatch) =>
bindActionCreators({
setCommenterStatus,
setRole,
+ viewUserDetail,
}, dispatch);
export default compose(
diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js
index 26543be6e..e37fa887c 100644
--- a/client/coral-admin/src/routes/Moderation/components/Comment.js
+++ b/client/coral-admin/src/routes/Moderation/components/Comment.js
@@ -1,22 +1,20 @@
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
-import Linkify from 'react-linkify';
import {Icon} from 'coral-ui';
-import FlagBox from './FlagBox';
+import FlagBox from 'coral-admin/src/components/FlagBox';
import styles from './styles.css';
-import CommentType from './CommentType';
-import Highlighter from 'react-highlight-words';
+import CommentType from 'coral-admin/src/components/CommentType';
+import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit';
import Slot from 'coral-framework/components/Slot';
import {getActionSummary} from 'coral-framework/utils';
import ActionButton from 'coral-admin/src/components/ActionButton';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
+import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
+import IfHasLink from 'coral-admin/src/components/IfHasLink';
import cn from 'classnames';
-import {murmur3} from 'murmurhash-js';
-import {CSSTransitionGroup} from 'react-transition-group';
-
-const linkify = new Linkify();
+import {getCommentType} from 'coral-admin/src/utils/comment';
import t, {timeago} from 'coral-framework/services/i18n';
@@ -29,41 +27,16 @@ class Comment extends React.Component {
viewUserDetail,
suspectWords,
bannedWords,
- minimal,
selected,
- toggleSelect,
className,
...props
} = this.props;
- const links = linkify.getMatches(comment.body);
- const linkText = links ? links.map((link) => link.raw) : [];
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
- const flagActions =
- comment.actions &&
- comment.actions.filter((a) => a.__typename === 'FlagAction');
- let commentType = '';
- if (comment.status === 'PREMOD') {
- commentType = 'premod';
- } else if (flagActions && flagActions.length) {
- commentType = 'flagged';
- }
+ const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
+ const commentType = getCommentType(comment);
- // since words are checked against word boundaries on the backend,
- // should be the behavior on the front end as well.
- // currently the highlighter plugin does not support out of the box.
- const searchWords = [...suspectWords, ...bannedWords]
- .filter((w) => {
- return new RegExp(`(^|\\s)${w}(\\s|$)`, 'i').test(comment.body);
- })
- .concat(linkText);
-
- let selectionStateCSS;
- if (minimal) {
- selectionStateCSS = selected ? styles.minimalSelection : '';
- } else {
- selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
- }
+ let selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
const showSuspenUserDialog = () => props.showSuspendUserDialog({
userId: comment.user.id,
@@ -81,31 +54,21 @@ class Comment extends React.Component {
return (
{
- !minimal && (
+ (
viewUserDetail(comment.user.id)}>
{comment.user.username}
)
}
- {
- minimal && typeof selected === 'boolean' && typeof toggleSelect === 'function' && (
- toggleSelect(e.target.value, e.target.checked)} />
- )
- }
- {timeago(comment.created_at || Date.now() - props.index * 60 * 1000)}
+ {timeago(comment.created_at)}
{
(comment.editing && comment.editing.edited)
@@ -125,45 +88,27 @@ class Comment extends React.Component {
}
-
+
- {comment.user.status === 'banned'
- ?
-
- {t('comment.banned_user')}
-
- : null}
-
+
Story: {comment.asset.title}
{!props.currentAsset &&
{t('modqueue.moderate')}}
-
-
+
+
-
{' '}
- {links
- ?
- Contains Link
-
- : null}
+
+
+ Contains Link
+
+
{actions.map((action, i) => {
const active =
@@ -193,7 +138,6 @@ class Comment extends React.Component {
(action === 'APPROVE' && comment.status === 'ACCEPTED');
return (
-
+
viewUserDetail(comment.user.id)}
+ viewUserDetail={viewUserDetail}
/>
: null}
@@ -240,7 +184,6 @@ class Comment extends React.Component {
}
Comment.propTypes = {
- minimal: PropTypes.bool,
viewUserDetail: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
@@ -251,7 +194,6 @@ 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/Moderation.js b/client/coral-admin/src/routes/Moderation/components/Moderation.js
index 35281df41..7a4130f70 100644
--- a/client/coral-admin/src/routes/Moderation/components/Moderation.js
+++ b/client/coral-admin/src/routes/Moderation/components/Moderation.js
@@ -6,7 +6,6 @@ import ModerationQueue from './ModerationQueue';
import ModerationMenu from './ModerationMenu';
import ModerationHeader from './ModerationHeader';
import ModerationKeysModal from '../../../components/ModerationKeysModal';
-import UserDetail from '../containers/UserDetail';
import StorySearch from '../containers/StorySearch';
import {isPremod, getModPath} from '../../../utils';
@@ -204,18 +203,6 @@ export default class Moderation extends Component {
open={moderation.modalOpen}
onClose={this.onClose}/>
- {moderation.userDetailId && (
-
- )}
-
({
toggleStorySearch,
showSuspendUserDialog,
viewUserDetail,
- hideUserDetail,
setSortOrder,
storySearchChange,
clearState
diff --git a/client/coral-admin/src/utils/comment.js b/client/coral-admin/src/utils/comment.js
new file mode 100644
index 000000000..26530f6c9
--- /dev/null
+++ b/client/coral-admin/src/utils/comment.js
@@ -0,0 +1,9 @@
+export function getCommentType(comment) {
+ let commentType = '';
+ if (comment.status === 'PREMOD') {
+ commentType = 'premod';
+ } else if (comment.actions && comment.actions.some((a) => a.__typename === 'FlagAction')) {
+ commentType = 'flagged';
+ }
+ return commentType;
+}
diff --git a/client/coral-admin/src/routes/Moderation/helpers/moderationQueueActionsMap.js b/client/coral-admin/src/utils/moderationQueueActionsMap.js
similarity index 100%
rename from client/coral-admin/src/routes/Moderation/helpers/moderationQueueActionsMap.js
rename to client/coral-admin/src/utils/moderationQueueActionsMap.js
diff --git a/client/coral-embed-stream/src/components/Comment.css b/client/coral-embed-stream/src/components/Comment.css
index 713655914..32a034f51 100644
--- a/client/coral-embed-stream/src/components/Comment.css
+++ b/client/coral-embed-stream/src/components/Comment.css
@@ -169,5 +169,5 @@
}
.footer {
-
+ min-height: 10px;
}
diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js
index 54bbfea50..8bece5dc5 100644
--- a/client/coral-embed-stream/src/components/Comment.js
+++ b/client/coral-embed-stream/src/components/Comment.js
@@ -317,6 +317,7 @@ export default class Comment extends React.Component {
} = this.props;
const view = this.getVisibileReplies();
+ const isActive = ['NONE', 'ACCEPTED'].indexOf(comment.status) >= 0;
const {loadingState} = this.state;
const isPending = comment.id.indexOf('pending') >= 0;
const isHighlighted = highlighted === comment.id;
@@ -422,7 +423,7 @@ export default class Comment extends React.Component {
{...slotProps}
/>
- { (currentUser && (comment.user.id === currentUser.id)) &&
+ { isActive && (currentUser && (comment.user.id === currentUser.id)) &&
/* User can edit/delete their own comment for a short window after posting */
@@ -467,44 +468,48 @@ export default class Comment extends React.Component {
}
-
-
-
- {!disableReply &&
-
-
+ {isActive &&
+
+
+
- }
-
-
+ {!disableReply &&
+
+
+ }
+
+
+
+ }
diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js
index 8e4e81274..66ffc0fc8 100644
--- a/client/coral-embed-stream/src/components/Stream.js
+++ b/client/coral-embed-stream/src/components/Stream.js
@@ -16,7 +16,7 @@ import QuestionBox from 'talk-plugin-questionbox/QuestionBox';
import {Button, TabBar, Tab, TabCount, TabContent, TabPane} from 'coral-ui';
import cn from 'classnames';
-import {getTopLevelParent} from '../graphql/utils';
+import {getTopLevelParent, attachCommentToParent} from '../graphql/utils';
import AllCommentsPane from './AllCommentsPane';
import styles from './Stream.css';
@@ -103,7 +103,16 @@ class Stream extends React.Component {
const open = asset.closedAt === null;
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
- const highlightedComment = comment && getTopLevelParent(comment);
+ let highlightedComment = comment && getTopLevelParent(comment);
+ if (highlightedComment) {
+ const isInactive = ['NONE', 'ACCEPTED'].indexOf(comment.status) === -1;
+ if (comment.parent && isInactive) {
+
+ // the highlighted comment is not active and as such not in the replies, so we
+ // attach it to the right parent.
+ highlightedComment = attachCommentToParent(highlightedComment, comment);
+ }
+ }
const banned = user && user.status === 'BANNED';
const temporarilySuspended =
diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js
index 804f3d37e..f97320c0a 100644
--- a/client/coral-embed-stream/src/containers/Stream.js
+++ b/client/coral-embed-stream/src/containers/Stream.js
@@ -265,8 +265,8 @@ const fragments = {
charCount
requireEmailConfirmation
}
- commentCount(excludeIgnored: $excludeIgnored)
- totalCommentCount(excludeIgnored: $excludeIgnored)
+ commentCount(excludeIgnored: $excludeIgnored) @skip(if: $hasComment)
+ totalCommentCount(excludeIgnored: $excludeIgnored) @skip(if: $hasComment)
comments(limit: 10, excludeIgnored: $excludeIgnored) @skip(if: $hasComment) {
nodes {
...CoralEmbedStream_Stream_comment
diff --git a/client/coral-embed-stream/src/graphql/utils.js b/client/coral-embed-stream/src/graphql/utils.js
index 1030a3547..ac6298616 100644
--- a/client/coral-embed-stream/src/graphql/utils.js
+++ b/client/coral-embed-stream/src/graphql/utils.js
@@ -185,6 +185,32 @@ export function insertFetchedCommentsIntoEmbedQuery(root, comments, parent_id) {
return applyToCommentsOrigin(root, (origin) => findAndInsertFetchedComments(origin, comments, parent_id));
}
+/**
+ * attachCommentToParent recurses through the comment tree starting at `topLevelComment`
+ * to find the parent of `comment` and attach it to the replies.
+ */
+export function attachCommentToParent(topLevelComment, comment) {
+ if (topLevelComment.id === comment.parent.id) {
+ return update(topLevelComment, {
+ replies: {
+ nodes: {
+ $apply: (nodes) => insertCommentsSorted(nodes, comment),
+ },
+ },
+ replyCount: {
+ $set: (count) => count + 1,
+ }
+ });
+ }
+ return update(topLevelComment, {
+ replies: {
+ nodes: {
+ $apply: (nodes) => nodes.map((node) => attachCommentToParent(node, comment)),
+ },
+ },
+ });
+}
+
/**
* Nest a string in itself repeatly until `level` has been reached.
*
diff --git a/graph/resolvers/asset.js b/graph/resolvers/asset.js
index da9114c6a..fb2f6cac5 100644
--- a/graph/resolvers/asset.js
+++ b/graph/resolvers/asset.js
@@ -1,13 +1,21 @@
const {decorateWithTags} = require('./util');
+const {
+ SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS,
+} = require('../../perms/constants');
const Asset = {
recentComments({id}, _, {loaders: {Comments}}) {
return Comments.genRecentComments.load(id);
},
- async comment({id}, {id: commentId}, {loaders: {Comments}}) {
+ async comment({id}, {id: commentId}, {loaders: {Comments}, user}) {
+ const statuses = user && user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS)
+ ? ['NONE', 'ACCEPTED', 'PREMOD', 'REJECTED']
+ : ['NONE', 'ACCEPTED'];
+
const comments = await Comments.getByQuery({
asset_id: id,
- ids: commentId
+ ids: commentId,
+ statuses,
});
return comments.nodes[0];
diff --git a/plugins/talk-plugin-featured-comments/client/containers/Tab.js b/plugins/talk-plugin-featured-comments/client/containers/Tab.js
index f28a63566..fee7f5da6 100644
--- a/plugins/talk-plugin-featured-comments/client/containers/Tab.js
+++ b/plugins/talk-plugin-featured-comments/client/containers/Tab.js
@@ -17,7 +17,7 @@ const enhance = compose(
withFragments({
asset: gql`
fragment TalkFeaturedComments_Tab_asset on Asset {
- featuredCommentsCount: totalCommentCount(tags: ["FEATURED"], excludeIgnored: $excludeIgnored)
+ featuredCommentsCount: totalCommentCount(tags: ["FEATURED"], excludeIgnored: $excludeIgnored) @skip(if: $hasComment)
}`,
}),
excludeIf((props) => props.asset.featuredCommentsCount === 0),
diff --git a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js
index 3a082e732..2195b0107 100644
--- a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js
+++ b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js
@@ -79,7 +79,7 @@ const enhance = compose(
asset: gql`
fragment TalkFeaturedComments_TabPane_asset on Asset {
id
- featuredComments: comments(tags: ["FEATURED"], excludeIgnored: $excludeIgnored, deep: true) {
+ featuredComments: comments(tags: ["FEATURED"], excludeIgnored: $excludeIgnored, deep: true) @skip(if: $hasComment) {
nodes {
...${getDefinitionName(Comment.fragments.comment)}
}
|