import React, {PropTypes} from 'react'; import PermalinkButton from 'coral-plugin-permalinks/PermalinkButton'; import AuthorName from 'coral-plugin-author-name/AuthorName'; import TagLabel from 'coral-plugin-tag-label/TagLabel'; import Content from 'coral-plugin-commentcontent/CommentContent'; import PubDate from 'coral-plugin-pubdate/PubDate'; import {ReplyBox, ReplyButton} from 'coral-plugin-replies'; import FlagComment from 'coral-plugin-flags/FlagComment'; import { BestButton, IfUserCanModifyBest, BEST_TAG, commentIsBest, BestIndicator } from 'coral-plugin-best/BestButton'; import Slot from 'coral-framework/components/Slot'; import LoadMore from './LoadMore'; import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import {TopRightMenu} from './TopRightMenu'; import classnames from 'classnames'; import {EditableCommentContent} from './EditableCommentContent'; import {getActionSummary, iPerformedThisAction} from 'coral-framework/utils'; import {getEditableUntilDate} from './util'; import styles from './Comment.css'; const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF'); // hold actions links (e.g. Reply) along the comment footer const ActionButton = ({children}) => { return ( {children} ); }; class Comment extends React.Component { constructor(props) { super(props); // timeout to keep track of Comment edit window expiration this.editWindowExpiryTimeout = null; this.onClickEdit = this.onClickEdit.bind(this); this.stopEditing = this.stopEditing.bind(this); this.state = { // Whether the comment should be editable (e.g. after a commenter clicking the 'Edit' button on their own comment) isEditing: false, replyBoxVisible: false, }; } static propTypes = { reactKey: PropTypes.string.isRequired, // id of currently opened ReplyBox. tracked in Stream.js activeReplyBox: PropTypes.string.isRequired, disableReply: PropTypes.bool, setActiveReplyBox: PropTypes.func.isRequired, showSignInDialog: PropTypes.func.isRequired, postFlag: PropTypes.func.isRequired, deleteAction: PropTypes.func.isRequired, parentId: PropTypes.string, highlighted: PropTypes.string, addNotification: PropTypes.func.isRequired, postComment: PropTypes.func.isRequired, depth: PropTypes.number.isRequired, asset: PropTypes.shape({ id: PropTypes.string, title: PropTypes.string, url: PropTypes.string }).isRequired, currentUser: PropTypes.shape({ id: PropTypes.string.isRequired }), charCountEnable: PropTypes.bool.isRequired, maxCharCount: PropTypes.number, 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.arrayOf( PropTypes.shape({ body: PropTypes.string.isRequired, id: PropTypes.string.isRequired }) ), user: PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string.isRequired }).isRequired, editing: PropTypes.shape({ edited: PropTypes.bool, // ISO8601 editableUntil: PropTypes.string, }) }).isRequired, // given a comment, return whether it should be rendered as ignored commentIsIgnored: React.PropTypes.func, // dispatch action to add a tag to a comment addCommentTag: React.PropTypes.func, // dispatch action to remove a tag from a comment removeCommentTag: React.PropTypes.func, // dispatch action to ignore another user ignoreUser: React.PropTypes.func, // edit a comment, passed (id, asset_id, { body }) editComment: React.PropTypes.func, } onClickEdit (e) { e.preventDefault(); this.setState({isEditing: true}); } stopEditing () { if (this._isMounted) { this.setState({isEditing: false}); } } componentDidMount() { this._isMounted = true; if (this.editWindowExpiryTimeout) { this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout); } // if still in the edit window, set a timeout to re-render once it expires const msLeftToEdit = editWindowRemainingMs(this.props.comment); if (msLeftToEdit > 0) { this.editWindowExpiryTimeout = setTimeout(() => { // re-render this.setState(this.state); }, msLeftToEdit); } } componentWillUnmount() { if (this.editWindowExpiryTimeout) { this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout); } this._isMounted = false; } render () { const { comment, parentId, currentUser, asset, depth, postComment, addNotification, showSignInDialog, highlighted, postFlag, postDontAgree, loadMore, setActiveReplyBox, activeReplyBox, deleteAction, addCommentTag, removeCommentTag, ignoreUser, disableReply, commentIsIgnored, maxCharCount, charCountEnable } = this.props; const flagSummary = getActionSummary('FlagActionSummary', comment); const dontAgreeSummary = getActionSummary( 'DontAgreeActionSummary', comment ); let myFlag = null; if (iPerformedThisAction('FlagActionSummary', comment)) { myFlag = flagSummary.find((s) => s.current_user); } else if (iPerformedThisAction('DontAgreeActionSummary', comment)) { myFlag = dontAgreeSummary.find((s) => s.current_user); } let commentClass = parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`; commentClass += comment.id === 'pending' ? ` ${styles.pendingComment}` : ''; // call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar) const notifyOnError = (fn, errorToMessage) => async function(...args) { if (typeof errorToMessage !== 'function') { errorToMessage = (error) => error.message; } try { return await fn(...args); } catch (error) { addNotification('error', errorToMessage(error)); throw error; } }; const addBestTag = notifyOnError( () => addCommentTag({ id: comment.id, tag: BEST_TAG }), () => 'Failed to tag comment as best' ); const removeBestTag = notifyOnError( () => removeCommentTag({ id: comment.id, tag: BEST_TAG }), () => 'Failed to remove best comment tag' ); return (