mirror of
https://github.com/wassname/talk.git
synced 2026-06-30 16:50:44 +08:00
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
<ReplyBox
|
||||
commentPostedHandler={this.commentPostedHandler}
|
||||
charCountEnable={charCountEnable}
|
||||
maxCharCount={maxCharCount}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
parentId={depth < THREADING_LEVEL ? comment.id : parentId}
|
||||
notify={notify}
|
||||
postComment={postComment}
|
||||
currentUser={currentUser}
|
||||
assetId={asset.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<TransitionGroup key="transitionGroup">
|
||||
{view.map(reply => {
|
||||
return (
|
||||
<CommentContainer
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
disableReply={disableReply}
|
||||
activeReplyBox={activeReplyBox}
|
||||
notify={notify}
|
||||
parentId={comment.id}
|
||||
postComment={postComment}
|
||||
editComment={this.props.editComment}
|
||||
depth={depth + 1}
|
||||
asset={asset}
|
||||
highlighted={highlighted}
|
||||
currentUser={currentUser}
|
||||
postFlag={postFlag}
|
||||
deleteAction={deleteAction}
|
||||
loadMore={loadMore}
|
||||
charCountEnable={charCountEnable}
|
||||
maxCharCount={maxCharCount}
|
||||
showSignInDialog={showSignInDialog}
|
||||
liveUpdates={liveUpdates}
|
||||
reactKey={reply.id}
|
||||
key={reply.id}
|
||||
comment={reply}
|
||||
emit={emit}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TransitionGroup>
|
||||
);
|
||||
}
|
||||
|
||||
if (!highlighted && this.commentIsRejected(comment)) {
|
||||
return (
|
||||
<CommentTombstone
|
||||
action="reject"
|
||||
onUndo={() => {
|
||||
this.props.setCommentStatus({
|
||||
commentId: comment.id,
|
||||
status:
|
||||
comment.status_history[comment.status_history.length - 2].type,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.commentIsIgnored(comment)) {
|
||||
return <CommentTombstone action="ignore" />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="talk-load-more-replies" key="loadMoreReplies">
|
||||
<LoadMore
|
||||
topLevel={false}
|
||||
replyCount={moreRepliesCount}
|
||||
moreComments={hasMoreComments}
|
||||
loadMore={this.loadNewReplies}
|
||||
loadingState={loadingState}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={rootClassName} id={`c_${comment.id}`}>
|
||||
<div className={commentClassName}>
|
||||
<Slot
|
||||
className={`${styles.commentAvatar} talk-stream-comment-avatar`}
|
||||
fill="commentAvatar"
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
inline
|
||||
/>
|
||||
<div className={commentClassName}>
|
||||
<Slot
|
||||
className={cn(styles.commentAvatar, 'talk-stream-comment-avatar')}
|
||||
fill="commentAvatar"
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
inline
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
styles.commentContainer,
|
||||
'talk-stream-comment-container'
|
||||
)}
|
||||
>
|
||||
<div className={cn(styles.header, 'talk-stream-comment-header')}>
|
||||
<div
|
||||
className={cn(
|
||||
styles.headerContainer,
|
||||
'talk-stream-comment-header-container'
|
||||
)}
|
||||
>
|
||||
<Slot
|
||||
className={cn(styles.username, 'talk-stream-comment-user-name')}
|
||||
fill="commentAuthorName"
|
||||
defaultComponent={CommentAuthorName}
|
||||
queryData={queryData}
|
||||
{...slotProps}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
styles.commentContainer,
|
||||
'talk-stream-comment-container'
|
||||
)}
|
||||
>
|
||||
<div className={cn(styles.header, 'talk-stream-comment-header')}>
|
||||
<div
|
||||
className={cn(
|
||||
styles.headerContainer,
|
||||
'talk-stream-comment-header-container'
|
||||
styles.tagsContainer,
|
||||
'talk-stream-comment-header-tags-container'
|
||||
)}
|
||||
>
|
||||
{isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
|
||||
|
||||
<Slot
|
||||
className={cn(
|
||||
styles.commentAuthorTagsSlot,
|
||||
'talk-stream-comment-author-tags'
|
||||
)}
|
||||
fill="commentAuthorTags"
|
||||
queryData={queryData}
|
||||
{...slotProps}
|
||||
inline
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
styles.bylineSecondary,
|
||||
'talk-stream-comment-user-byline'
|
||||
)}
|
||||
>
|
||||
<Slot
|
||||
className={cn(
|
||||
styles.username,
|
||||
'talk-stream-comment-user-name'
|
||||
)}
|
||||
fill="commentAuthorName"
|
||||
defaultComponent={CommentAuthorName}
|
||||
fill="commentTimestamp"
|
||||
defaultComponent={CommentTimestamp}
|
||||
className={'talk-stream-comment-published-date'}
|
||||
created_at={comment.created_at}
|
||||
queryData={queryData}
|
||||
{...slotProps}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
styles.tagsContainer,
|
||||
'talk-stream-comment-header-tags-container'
|
||||
)}
|
||||
>
|
||||
{isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
|
||||
|
||||
<Slot
|
||||
className={cn(
|
||||
styles.commentAuthorTagsSlot,
|
||||
'talk-stream-comment-author-tags'
|
||||
)}
|
||||
fill="commentAuthorTags"
|
||||
queryData={queryData}
|
||||
{...slotProps}
|
||||
inline
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={`${
|
||||
styles.bylineSecondary
|
||||
} talk-stream-comment-user-byline`}
|
||||
>
|
||||
<Slot
|
||||
fill="commentTimestamp"
|
||||
defaultComponent={CommentTimestamp}
|
||||
className={'talk-stream-comment-published-date'}
|
||||
created_at={comment.created_at}
|
||||
queryData={queryData}
|
||||
{...slotProps}
|
||||
/>
|
||||
{comment.editing && comment.editing.edited ? (
|
||||
<span>
|
||||
<span className={styles.editedMarker}>
|
||||
({t('comment.edited')})
|
||||
</span>
|
||||
{comment.editing && comment.editing.edited ? (
|
||||
<span>
|
||||
<span className={styles.editedMarker}>
|
||||
({t('comment.edited')})
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Slot
|
||||
className={styles.commentInfoBar}
|
||||
fill="commentInfoBar"
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
/>
|
||||
|
||||
{isActive &&
|
||||
(currentUser && comment.user.id === currentUser.id) && (
|
||||
/* User can edit/delete their own comment for a short window after posting */
|
||||
<span className={cn(styles.topRight)}>
|
||||
{this.state.isEditable && (
|
||||
<a
|
||||
className={cn(styles.link, {
|
||||
[styles.active]: this.state.isEditing,
|
||||
})}
|
||||
onClick={this.onClickEdit}
|
||||
>
|
||||
Edit
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{!isActive && <InactiveCommentLabel status={comment.status} />}
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{this.state.isEditing ? (
|
||||
<EditableCommentContent
|
||||
editComment={this.editComment}
|
||||
notify={notify}
|
||||
comment={comment}
|
||||
currentUser={currentUser}
|
||||
charCountEnable={charCountEnable}
|
||||
maxCharCount={maxCharCount}
|
||||
parentId={parentId}
|
||||
stopEditing={this.stopEditing}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<Slot
|
||||
fill="commentContent"
|
||||
className="talk-stream-comment-content"
|
||||
defaultComponent={CommentContent}
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
slotSize={1}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={cn(styles.footer, 'talk-stream-comment-footer')}>
|
||||
{isActive && (
|
||||
<div className={'talk-stream-comment-actions-container'}>
|
||||
<div className="talk-embed-stream-comment-actions-container-left commentActionsLeft comment__action-container">
|
||||
<Slot
|
||||
fill="commentReactions"
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
inline
|
||||
/>
|
||||
|
||||
{!disableReply && (
|
||||
<ActionButton>
|
||||
<ReplyButton
|
||||
onClick={this.showReplyBox}
|
||||
parentCommentId={parentId || comment.id}
|
||||
currentUserId={currentUser && currentUser.id}
|
||||
/>
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="talk-embed-stream-comment-actions-container-right commentActionsRight comment__action-container">
|
||||
<Slot
|
||||
fill="commentActions"
|
||||
wrapperComponent={ActionButton}
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
inline
|
||||
/>
|
||||
<ActionButton>
|
||||
<FlagComment
|
||||
flaggedByCurrentUser={!!myFlag}
|
||||
flag={myFlag}
|
||||
id={comment.id}
|
||||
author_id={comment.user.id}
|
||||
postFlag={postFlag}
|
||||
notify={notify}
|
||||
postDontAgree={postDontAgree}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
<Slot
|
||||
className={styles.commentInfoBar}
|
||||
fill="commentInfoBar"
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
/>
|
||||
|
||||
{isActive &&
|
||||
(currentUser && comment.user.id === currentUser.id) && (
|
||||
/* User can edit/delete their own comment for a short window after posting */
|
||||
<span className={cn(styles.topRight)}>
|
||||
{this.state.isEditable && (
|
||||
<a
|
||||
className={cn(styles.link, {
|
||||
[styles.active]: this.state.isEditing,
|
||||
})}
|
||||
onClick={this.onClickEdit}
|
||||
>
|
||||
Edit
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!isActive && <InactiveCommentLabel status={comment.status} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeReplyBox === comment.id ? (
|
||||
<ReplyBox
|
||||
commentPostedHandler={this.commentPostedHandler}
|
||||
charCountEnable={charCountEnable}
|
||||
maxCharCount={maxCharCount}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
parentId={depth < THREADING_LEVEL ? comment.id : parentId}
|
||||
notify={notify}
|
||||
postComment={postComment}
|
||||
currentUser={currentUser}
|
||||
assetId={asset.id}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<TransitionGroup>
|
||||
{view.map(reply => {
|
||||
return (
|
||||
<CommentContainer
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
disableReply={disableReply}
|
||||
activeReplyBox={activeReplyBox}
|
||||
<div className={styles.content}>
|
||||
{this.state.isEditing ? (
|
||||
<EditableCommentContent
|
||||
editComment={this.editComment}
|
||||
notify={notify}
|
||||
parentId={comment.id}
|
||||
postComment={postComment}
|
||||
editComment={this.props.editComment}
|
||||
depth={depth + 1}
|
||||
asset={asset}
|
||||
highlighted={highlighted}
|
||||
comment={comment}
|
||||
currentUser={currentUser}
|
||||
postFlag={postFlag}
|
||||
deleteAction={deleteAction}
|
||||
loadMore={loadMore}
|
||||
charCountEnable={charCountEnable}
|
||||
maxCharCount={maxCharCount}
|
||||
showSignInDialog={showSignInDialog}
|
||||
liveUpdates={liveUpdates}
|
||||
reactKey={reply.id}
|
||||
key={reply.id}
|
||||
comment={reply}
|
||||
emit={emit}
|
||||
parentId={parentId}
|
||||
stopEditing={this.stopEditing}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TransitionGroup>
|
||||
<div className="talk-load-more-replies">
|
||||
<LoadMore
|
||||
topLevel={false}
|
||||
replyCount={moreRepliesCount}
|
||||
moreComments={hasMoreComments}
|
||||
loadMore={this.loadNewReplies}
|
||||
loadingState={loadingState}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<Slot
|
||||
fill="commentContent"
|
||||
className="talk-stream-comment-content"
|
||||
defaultComponent={CommentContent}
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
slotSize={1}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={cn(styles.footer, 'talk-stream-comment-footer')}>
|
||||
{isActive && (
|
||||
<div className={'talk-stream-comment-actions-container'}>
|
||||
<div className="talk-embed-stream-comment-actions-container-left commentActionsLeft comment__action-container">
|
||||
<Slot
|
||||
fill="commentReactions"
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
inline
|
||||
/>
|
||||
|
||||
{!disableReply && (
|
||||
<ActionButton>
|
||||
<ReplyButton
|
||||
onClick={this.showReplyBox}
|
||||
parentCommentId={parentId || comment.id}
|
||||
currentUserId={currentUser && currentUser.id}
|
||||
/>
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="talk-embed-stream-comment-actions-container-right commentActionsRight comment__action-container">
|
||||
<Slot
|
||||
fill="commentActions"
|
||||
wrapperComponent={ActionButton}
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
inline
|
||||
/>
|
||||
<ActionButton>
|
||||
<FlagComment
|
||||
flaggedByCurrentUser={!!myFlag}
|
||||
flag={myFlag}
|
||||
id={comment.id}
|
||||
author_id={comment.user.id}
|
||||
postFlag={postFlag}
|
||||
notify={notify}
|
||||
postDontAgree={postDontAgree}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
depth,
|
||||
comment,
|
||||
activeReplyBox,
|
||||
highlighted,
|
||||
animateEnter,
|
||||
} = this.props;
|
||||
|
||||
if (!highlighted && this.commentIsRejected(comment)) {
|
||||
return <CommentTombstone action="reject" onUndo={this.undoStatus} />;
|
||||
}
|
||||
|
||||
if (this.commentIsIgnored(comment)) {
|
||||
return <CommentTombstone action="ignore" />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={rootClassName} id={id}>
|
||||
{this.renderComment()}
|
||||
{activeReplyBox === comment.id && this.renderReplyBox()}
|
||||
{this.renderRepliesContainer()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// return whether the comment is editable
|
||||
|
||||
@@ -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 (
|
||||
@@ -112,7 +102,7 @@ class Stream extends React.Component {
|
||||
postComment={postComment}
|
||||
asset={asset}
|
||||
currentUser={currentUser}
|
||||
highlighted={comment.id}
|
||||
highlighted={comment}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
loadMore={loadNewReplies}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
`,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user