diff --git a/client/coral-embed-stream/src/graphql/utils.js b/client/coral-embed-stream/src/graphql/utils.js
index 720cb7f2e..96cf49cad 100644
--- a/client/coral-embed-stream/src/graphql/utils.js
+++ b/client/coral-embed-stream/src/graphql/utils.js
@@ -141,7 +141,7 @@ export function findCommentInAsset(asset, callbackOrId) {
callback = node => node.id === callbackOrId;
}
if (asset.comment) {
- return findComment([getTopLevelParent(asset.comment)], callback);
+ return findComment([reverseCommentParentTree(asset.comment)], callback);
}
if (!asset.comments) {
return false;
@@ -187,11 +187,12 @@ export function insertFetchedCommentsIntoEmbedQuery(root, 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) {
+function attachComment(topLevelComment, comment) {
+ if (!topLevelComment.replies) {
+ topLevelComment = update(topLevelComment, {
+ replies: { $set: { nodes: [] } },
+ });
+ }
if (topLevelComment.id === comment.parent.id) {
return update(topLevelComment, {
replies: {
@@ -204,16 +205,40 @@ export function attachCommentToParent(topLevelComment, comment) {
},
});
}
+ if (!topLevelComment.replies.nodes.length) {
+ return topLevelComment;
+ }
return update(topLevelComment, {
replies: {
nodes: {
- $apply: nodes =>
- nodes.map(node => attachCommentToParent(node, comment)),
+ $apply: nodes => nodes.map(node => attachComment(node, comment)),
},
},
});
}
+/**
+ * attachCommentToParent recurses through the comment tree starting at `topLevelComment`
+ * to find the ancestor of `comment` and attach it to the replies.
+ */
+export function attachCommentToParent(topLevelComment, comment) {
+ let result = topLevelComment;
+ if (comment.parent.parent) {
+ result = attachCommentToParent(result, comment.parent);
+ }
+ return attachComment(result, comment);
+}
+
+/**
+ * reverseCommentParentTree reverses a comment parent relationship tree
+ * like `comment -> parent -> parent` into `parent -> parent -> comment -> replies`.
+ */
+export function reverseCommentParentTree(comment) {
+ return comment.parent
+ ? attachCommentToParent(getTopLevelParent(comment), comment)
+ : comment;
+}
+
/**
* Nest a string in itself repeatly until `level` has been reached.
*
diff --git a/client/coral-embed-stream/src/tabs/stream/components/Comment.css b/client/coral-embed-stream/src/tabs/stream/components/Comment.css
index 15d42bef0..58ff0e731 100644
--- a/client/coral-embed-stream/src/tabs/stream/components/Comment.css
+++ b/client/coral-embed-stream/src/tabs/stream/components/Comment.css
@@ -22,6 +22,10 @@
.commentLevel0 {
padding-left: 0px;
+
+ &.highlightedComment {
+ margin-top: 8px;
+ }
}
.commentLevel1 {
@@ -41,8 +45,8 @@
}
.highlightedComment {
- padding-left: 15px;
- border-left: 3px solid rgb(35,118,216);
+ padding: 1px 15px 8px 15px;
+ background-color: #E3F2FD;
}
.bylineSecondary {
diff --git a/client/coral-embed-stream/src/tabs/stream/components/Comment.js b/client/coral-embed-stream/src/tabs/stream/components/Comment.js
index f0dcc1017..f46630e97 100644
--- a/client/coral-embed-stream/src/tabs/stream/components/Comment.js
+++ b/client/coral-embed-stream/src/tabs/stream/components/Comment.js
@@ -13,6 +13,7 @@ import styles from './Comment.css';
import { THREADING_LEVEL } from '../../../constants/stream';
import merge from 'lodash/merge';
import mapValues from 'lodash/mapValues';
+import get from 'lodash/get';
import LoadMore from './LoadMore';
import { getEditableUntilDate } from './util';
@@ -169,7 +170,7 @@ export default class Comment extends React.Component {
postFlag: PropTypes.func.isRequired,
deleteAction: PropTypes.func.isRequired,
parentId: PropTypes.string,
- highlighted: PropTypes.string,
+ highlighted: PropTypes.object,
notify: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
depth: PropTypes.number.isRequired,
@@ -186,28 +187,7 @@ export default class Comment extends React.Component {
postDontAgree: PropTypes.func,
animateEnter: PropTypes.bool,
commentClassNames: PropTypes.array,
- comment: PropTypes.shape({
- depth: PropTypes.number,
- action_summaries: PropTypes.array.isRequired,
- body: PropTypes.string.isRequired,
- id: PropTypes.string.isRequired,
- tags: PropTypes.arrayOf(
- PropTypes.shape({
- name: PropTypes.string,
- })
- ),
- replies: PropTypes.object,
- user: PropTypes.shape({
- id: PropTypes.string.isRequired,
- username: PropTypes.string.isRequired,
- }).isRequired,
- editing: PropTypes.shape({
- edited: PropTypes.bool,
-
- // ISO8601
- editableUntil: PropTypes.string,
- }),
- }).isRequired,
+ comment: PropTypes.object.isRequired,
setCommentStatus: PropTypes.func.isRequired,
// edit a comment, passed (id, asset_id, { body })
@@ -301,6 +281,14 @@ export default class Comment extends React.Component {
this.props.setActiveReplyBox('');
};
+ undoStatus = () =>
+ this.props.setCommentStatus({
+ commentId: this.props.comment.id,
+ status: this.props.comment.status_history[
+ this.props.comment.status_history.length - 2
+ ].type,
+ });
+
// getVisibileReplies returns a list containing comments
// which were authored by current user or comes before the `idCursor`.
getVisibileReplies() {
@@ -329,6 +317,27 @@ export default class Comment extends React.Component {
return view;
}
+ /**
+ * getConditionalClassNames
+ * conditionalClassNames adds classNames based on condition
+ * classnames is an array of objects with key as classnames and value as conditions
+ * i.e:
+ * {
+ * 'myClassName': { tags: [STAFF]}
+ * }
+ *
+ * This will add myClassName to comments tagged with STAFF TAG.
+ **/
+ getConditionalClassNames() {
+ const { commentClassNames = [] } = this.props;
+ return mapValues(merge({}, ...commentClassNames), condition => {
+ if (condition.tags) {
+ return condition.tags.some(tag => hasTag(this.props.comment.tags, tag));
+ }
+ return false;
+ });
+ }
+
componentDidMount() {
this._isMounted = true;
if (this.editWindowExpiryTimeout) {
@@ -343,25 +352,51 @@ export default class Comment extends React.Component {
}, Math.max(msLeftToEdit, 0));
}
}
+
componentWillUnmount() {
if (this.editWindowExpiryTimeout) {
this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
}
this._isMounted = false;
}
- render() {
+
+ renderReplyBox() {
+ const {
+ asset,
+ depth,
+ comment,
+ parentId,
+ postComment,
+ currentUser,
+ setActiveReplyBox,
+ maxCharCount,
+ notify,
+ charCountEnable,
+ } = this.props;
+ return (
+
+ );
+ }
+
+ renderReplies(view) {
const {
asset,
- data,
- root,
depth,
comment,
postFlag,
- parentId,
highlighted,
postComment,
currentUser,
- postDontAgree,
setActiveReplyBox,
activeReplyBox,
loadMore,
@@ -372,39 +407,47 @@ export default class Comment extends React.Component {
charCountEnable,
showSignInDialog,
liveUpdates,
- animateEnter,
emit,
- commentClassNames = [],
} = this.props;
+ return (
+
+ {view.map(reply => {
+ return (
+
+ );
+ })}
+
+ );
+ }
- if (!highlighted && this.commentIsRejected(comment)) {
- return (
- {
- this.props.setCommentStatus({
- commentId: comment.id,
- status:
- comment.status_history[comment.status_history.length - 2].type,
- });
- }}
- />
- );
- }
-
- if (this.commentIsIgnored(comment)) {
- return ;
- }
-
- const view = this.getVisibileReplies();
-
- // Inactive comments can be viewed by moderators and admins (e.g. using permalinks).
- const isActive = isCommentActive(comment.status);
-
+ renderLoadMoreReplies(view) {
+ const { comment } = this.props;
const { loadingState } = this.state;
- const isPending = comment.id.indexOf('pending') >= 0;
- const isHighlighted = highlighted === comment.id;
-
const hasMoreComments =
comment.replies &&
(comment.replies.hasNextPage ||
@@ -412,6 +455,57 @@ export default class Comment extends React.Component {
const moreRepliesCount = this.hasIgnoredReplies()
? -1
: comment.replyCount - view.length;
+ return (
+
+
+
+ );
+ }
+
+ renderRepliesContainer() {
+ const { highlighted, comment } = this.props;
+
+ // Only render highlighted reply when we are the parent of it.
+ if (get(highlighted, 'parent.id') === comment.id) {
+ return this.renderReplies([highlighted]);
+ }
+
+ // Otherwise render replies in current view and a load more button if needed.
+ const view = this.getVisibileReplies();
+ return [this.renderReplies(view), this.renderLoadMoreReplies(view)];
+ }
+
+ renderComment() {
+ const {
+ asset,
+ data,
+ root,
+ depth,
+ comment,
+ postFlag,
+ parentId,
+ highlighted,
+ currentUser,
+ postDontAgree,
+ deleteAction,
+ disableReply,
+ maxCharCount,
+ notify,
+ charCountEnable,
+ showSignInDialog,
+ } = this.props;
+
+ // Inactive comments can be viewed by moderators and admins (e.g. using permalinks).
+ const isActive = isCommentActive(comment.status);
+
+ const isPending = comment.id.indexOf('pending') >= 0;
+ const isHighlighted = highlighted && highlighted.id === comment.id;
const flagSummary = getActionSummary('FlagActionSummary', comment);
const dontAgreeSummary = getActionSummary(
'DontAgreeActionSummary',
@@ -424,38 +518,6 @@ export default class Comment extends React.Component {
myFlag = dontAgreeSummary.find(s => s.current_user);
}
- /**
- * conditionClassNames
- * adds classNames based on condition
- * classnames is an array of objects with key as classnames and value as conditions
- * i.e:
- * {
- * 'myClassName': { tags: [STAFF]}
- * }
- *
- * This will add myClassName to comments tagged with STAFF TAG.
- * **/
- const conditionalClassNames = mapValues(
- merge({}, ...commentClassNames),
- condition => {
- if (condition.tags) {
- return condition.tags.some(tag => hasTag(comment.tags, tag));
- }
- return false;
- }
- );
-
- const rootClassName = cn(
- 'talk-stream-comment-wrapper',
- `talk-stream-comment-wrapper-level-${depth}`,
- styles.root,
- styles[`rootLevel${depth}`],
- {
- ...conditionalClassNames,
- [styles.enter]: animateEnter,
- }
- );
-
const commentClassName = cn(
'talk-stream-comment',
`talk-stream-comment-level-${depth}`,
@@ -482,241 +544,220 @@ export default class Comment extends React.Component {
};
return (
-