From ba62cdeab6651027881b1f72bdc8d0943ac8f7bb Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 27 Feb 2018 22:27:57 +0100 Subject: [PATCH 1/3] Change highlight styling --- .../coral-embed-stream/src/tabs/stream/components/Comment.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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..8945bc790 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/Comment.css +++ b/client/coral-embed-stream/src/tabs/stream/components/Comment.css @@ -41,8 +41,8 @@ } .highlightedComment { - padding-left: 15px; - border-left: 3px solid rgb(35,118,216); + padding: 1px 15px 8px 15px; + background-color: #E3F2FD; } .bylineSecondary { From 8aad7448187b549c7815e3ced4bb0737abafe49f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 27 Feb 2018 23:37:44 +0100 Subject: [PATCH 2/3] Only show highlighted comment --- .../src/tabs/stream/components/Comment.js | 640 ++++++++++-------- .../src/tabs/stream/components/Stream.js | 2 +- 2 files changed, 341 insertions(+), 301 deletions(-) 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..d75789a63 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/Comment.js +++ b/client/coral-embed-stream/src/tabs/stream/components/Comment.js @@ -169,7 +169,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 +186,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 +280,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 +316,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 +351,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 +406,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 +454,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 (highlighted && 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 +517,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 +543,220 @@ export default class Comment extends React.Component { }; return ( -
-
- +
+ + +
+
+
+ -
-
+ {isStaff(comment.tags) ? Staff : null} + + +
+ + - -
- {isStaff(comment.tags) ? Staff : null} - - -
- - - - {comment.editing && comment.editing.edited ? ( - -   - ({t('comment.edited')}) - + {comment.editing && comment.editing.edited ? ( + +   + ({t('comment.edited')}) - ) : null} - -
- - - - {isActive && - (currentUser && comment.user.id === currentUser.id) && ( - /* User can edit/delete their own comment for a short window after posting */ - - {this.state.isEditable && ( - - Edit - - )} - )} - {!isActive && } + ) : null} +
-
- {this.state.isEditing ? ( - - ) : ( -
- -
- )} -
-
- {isActive && ( -
-
- - {!disableReply && ( - - - - )} -
-
- - - - -
-
+ + + {isActive && + (currentUser && comment.user.id === currentUser.id) && ( + /* User can edit/delete their own comment for a short window after posting */ + + {this.state.isEditable && ( + + Edit + + )} + )} -
+ {!isActive && }
-
- - {activeReplyBox === comment.id ? ( - - ) : null} - - - {view.map(reply => { - return ( - + {this.state.isEditing ? ( + - ); - })} - -
- + ) : ( +
+ +
+ )} +
+
+ {isActive && ( +
+
+ + + {!disableReply && ( + + + + )} +
+
+ + + + +
+
+ )} +
); } + + render() { + const { + depth, + comment, + activeReplyBox, + highlighted, + animateEnter, + } = this.props; + + if (!highlighted && this.commentIsRejected(comment)) { + return ; + } + + if (this.commentIsIgnored(comment)) { + return ; + } + + const rootClassName = cn( + 'talk-stream-comment-wrapper', + `talk-stream-comment-wrapper-level-${depth}`, + styles.root, + styles[`rootLevel${depth}`], + { + ...this.getConditionalClassNames(), + [styles.enter]: animateEnter, + } + ); + + const id = `c_${comment.id}`; + + return ( +
+ {this.renderComment()} + {activeReplyBox === comment.id && this.renderReplyBox()} + {this.renderRepliesContainer()} +
+ ); + } } // return whether the comment is editable diff --git a/client/coral-embed-stream/src/tabs/stream/components/Stream.js b/client/coral-embed-stream/src/tabs/stream/components/Stream.js index a431b1986..8a6b75be3 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/Stream.js +++ b/client/coral-embed-stream/src/tabs/stream/components/Stream.js @@ -112,7 +112,7 @@ class Stream extends React.Component { postComment={postComment} asset={asset} currentUser={currentUser} - highlighted={comment.id} + highlighted={comment} postFlag={postFlag} postDontAgree={postDontAgree} loadMore={loadNewReplies} From 91dd9b157af0ce2fd9a0266877c78ff9d173095f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 2 Mar 2018 00:00:51 +0100 Subject: [PATCH 3/3] Implement new permlink view for real --- .../coral-embed-stream/src/graphql/utils.js | 41 +++++++++++++++---- .../src/tabs/stream/components/Comment.css | 4 ++ .../src/tabs/stream/components/Comment.js | 3 +- .../src/tabs/stream/components/Stream.js | 20 +++------ .../src/tabs/stream/containers/Comment.js | 2 +- .../src/tabs/stream/containers/Stream.js | 25 ++++++++--- 6 files changed, 64 insertions(+), 31 deletions(-) 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 8945bc790..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 { 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 d75789a63..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'; @@ -471,7 +472,7 @@ export default class Comment extends React.Component { const { highlighted, comment } = this.props; // Only render highlighted reply when we are the parent of it. - if (highlighted && highlighted.parent.id === comment.id) { + if (get(highlighted, 'parent.id') === comment.id) { return this.renderReplies([highlighted]); } diff --git a/client/coral-embed-stream/src/tabs/stream/components/Stream.js b/client/coral-embed-stream/src/tabs/stream/components/Stream.js index 8a6b75be3..c8a304214 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/Stream.js +++ b/client/coral-embed-stream/src/tabs/stream/components/Stream.js @@ -12,15 +12,11 @@ import RestrictedMessageBox from 'coral-framework/components/RestrictedMessageBo import t, { timeago } from 'coral-framework/services/i18n'; import CommentBox from '../containers/CommentBox'; import QuestionBox from '../../../components/QuestionBox'; -import { isCommentActive } from 'coral-framework/utils'; import { Tab, TabCount, TabPane } from 'coral-ui'; import cn from 'classnames'; import get from 'lodash/get'; -import { - getTopLevelParent, - attachCommentToParent, -} from '../../../graphql/utils'; +import { reverseCommentParentTree } from '../../../graphql/utils'; import AllCommentsPane from './AllCommentsPane'; import ExtendableTabPanel from '../../../containers/ExtendableTabPanel'; @@ -64,16 +60,10 @@ class Stream extends React.Component { viewAllComments, } = this.props; - // even though the permalinked comment is the highlighted one, we're displaying its parent + replies - let topLevelComment = getTopLevelParent(comment); - if (topLevelComment) { - // Inactive comments can be viewed by moderators and admins (e.g. using permalinks). - const isInactive = !isCommentActive(comment.status); - 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. - topLevelComment = attachCommentToParent(topLevelComment, comment); - } + let topLevelComment = null; + if (comment) { + // Reverse the comment tree that we get from bottom-top (comment -> parent) to top-bottom (parent -> comment) + topLevelComment = reverseCommentParentTree(comment); } return ( diff --git a/client/coral-embed-stream/src/tabs/stream/containers/Comment.js b/client/coral-embed-stream/src/tabs/stream/containers/Comment.js index 5a6c332e0..03a816107 100644 --- a/client/coral-embed-stream/src/tabs/stream/containers/Comment.js +++ b/client/coral-embed-stream/src/tabs/stream/containers/Comment.js @@ -64,7 +64,7 @@ const withAnimateEnter = hoistStatics(BaseComponent => { return WithAnimateEnter; }); -const singleCommentFragment = gql` +export const singleCommentFragment = gql` fragment CoralEmbedStream_Comment_SingleComment on Comment { id body diff --git a/client/coral-embed-stream/src/tabs/stream/containers/Stream.js b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js index e80719646..191490914 100644 --- a/client/coral-embed-stream/src/tabs/stream/containers/Stream.js +++ b/client/coral-embed-stream/src/tabs/stream/containers/Stream.js @@ -24,7 +24,7 @@ import { viewAllComments, } from '../../../actions/stream'; import Stream from '../components/Stream'; -import Comment from './Comment'; +import { default as Comment, singleCommentFragment } from './Comment'; import { withFragments, withEmit } from 'coral-framework/hocs'; import { getDefinitionName, @@ -282,7 +282,7 @@ StreamContainer.propTypes = { previousTab: PropTypes.string, }; -const commentFragment = gql` +const streamCommentFragment = gql` fragment CoralEmbedStream_Stream_comment on Comment { id status @@ -294,6 +294,18 @@ const commentFragment = gql` ${Comment.fragments.comment} `; +const streamSingleCommentFragment = gql` + fragment CoralEmbedStream_Stream_singleComment on Comment { + id + status + user { + id + } + ...${getDefinitionName(singleCommentFragment)} + } + ${singleCommentFragment} +`; + const COMMENTS_ADDED_SUBSCRIPTION = gql` subscription CommentAdded($assetId: ID!, $excludeIgnored: Boolean) { commentAdded(asset_id: $assetId) { @@ -303,7 +315,7 @@ const COMMENTS_ADDED_SUBSCRIPTION = gql` ...CoralEmbedStream_Stream_comment } } - ${commentFragment} + ${streamCommentFragment} `; const COMMENTS_EDITED_SUBSCRIPTION = gql` @@ -351,7 +363,7 @@ const LOAD_MORE_QUERY = gql` endCursor } } - ${commentFragment} + ${streamCommentFragment} `; const slots = [ @@ -398,7 +410,7 @@ const fragments = { ${nest( ` parent { - ...CoralEmbedStream_Stream_comment + ...CoralEmbedStream_Stream_singleComment ...nest } `, @@ -437,7 +449,8 @@ const fragments = { ...${getDefinitionName(Comment.fragments.asset)} } ${Comment.fragments.asset} - ${commentFragment} + ${streamCommentFragment} + ${streamSingleCommentFragment} `, };