text: import React from 'react';
  import PropTypes from 'prop-types';
  
  import TagLabel from './TagLabel';
  import CommentTimestamp from 'coral-framework/components/CommentTimestamp';
  import ReplyButton from './ReplyButton';
  import ReplyBox from '../containers/ReplyBox';
  import FlagComment from './FlagComment';
  import { can } from 'coral-framework/services/perms';
  import { TransitionGroup } from 'react-transition-group';
  import cn from 'classnames';
  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';
  import { findCommentWithId } from '../../../graphql/utils';
  import CommentContent from 'coral-framework/components/CommentContent';
  import Slot from 'coral-framework/components/Slot';
  import CommentTombstone from './CommentTombstone';
  import InactiveCommentLabel from './InactiveCommentLabel';
  import EditableCommentContent from '../containers/EditableCommentContent';
  import {
    getActionSummary,
    iPerformedThisAction,
    isCommentActive,
    getShallowChanges,
  } from 'coral-framework/utils';
  import t from 'coral-framework/services/i18n';
  import CommentContainer from '../containers/Comment';
  import { CommentAuthorName } from 'coral-framework/components';
  import { STAFF } from 'coral-framework/constants/roles';
  
  const isStaff = tags => !tags.every(t => t.tag.name !== STAFF);
  const hasTag = (tags, lookupTag) =>
    !!tags.filter(t => t.tag.name === lookupTag).length;
  const hasComment = (nodes, id) => nodes.some(node => node.id === id);
  
  // resetCursors will return the id cursors of the first and second newest comment in
  // the current reply list. The cursors are used to dertermine which
  // comments to show. The spare cursor functions as a backup in case one
  // of the comments gets deleted.
  function resetCursors(state, props) {
    const replies = props.comment.replies;
    if (replies && replies.nodes.length) {
      const idCursors = [replies.nodes[replies.nodes.length - 1].id];
      if (replies.nodes.length >= 2) {
        idCursors.push(replies.nodes[replies.nodes.length - 2].id);
      }
      return { idCursors };
    }
    return { idCursors: [] };
  }
  
  // invalidateCursor is called whenever a comment is removed which is referenced
  // by one of the 2 id cursors. It returns a new set of id cursors calculated
  // using the help of the backup cursor.
  function invalidateCursor(invalidated, state, props) {
    const alt = invalidated === 1 ? 0 : 1;
    const replies = props.comment.replies;
    const idCursors = [];
    if (state.idCursors[alt]) {
      idCursors.push(state.idCursors[alt]);
      const index = replies.nodes.findIndex(node => node.id === idCursors[0]);
      const prevInLine = replies.nodes[index - 1];
      if (prevInLine) {
        idCursors.push(prevInLine.id);
      }
    }
    return { idCursors };
  }
  
  // hold actions links (e.g. Reply) along the comment footer
  const ActionButton = ({ children }) => {
    return (
      <span className="comment__action-button comment__action-button--nowrap">
        {children}
      </span>
    );
  };
  
  ActionButton.propTypes = {
    // id of currently opened ReplyBox. tracked in Stream.js
    children: PropTypes.node,
  };
  
  // Determine whether the comment with id is in the part of the comments tree.
  function containsCommentId(props, id) {
    if (props.comment.id === id) {
      return true;
    }
    if (props.comment.replies) {
      return findCommentWithId(props.comment.replies.nodes, id);
    }
    return false;
  }
  
  export default 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 = {
        isEditable: commentIsStillEditable(props.comment),
        isEditing: false,
        replyBoxVisible: false,
        loadingState: '',
        ...resetCursors({}, props),
      };
    }
  
    componentWillReceiveProps(next) {
      const { comment: { replies: prevReplies } } = this.props;
      const { comment: { replies: nextReplies } } = next;
      if (
        prevReplies &&
        nextReplies &&
        nextReplies.nodes.length < prevReplies.nodes.length
      ) {
        // Invalidate first cursor if referenced comment was removed.
        if (
          this.state.idCursors[0] &&
          !hasComment(nextReplies.nodes, this.state.idCursors[0])
        ) {
          this.setState(invalidateCursor(0, this.state, next));
        }
  
        // Invalidate second cursor if referenced comment was removed.
        if (
          this.state.idCursors[1] &&
          !hasComment(nextReplies.nodes, this.state.idCursors[1])
        ) {
          this.setState(invalidateCursor(1, this.state, next));
        }
      }
    }
  
    shouldComponentUpdate(nextProps, nextState) {
      // Specifically handle `activeReplyBox` if it is the only change.
      const changes = [
        ...getShallowChanges(this.props, nextProps),
        ...getShallowChanges(this.state, nextState),
      ];
      if (changes.length === 1 && changes[0] === 'activeReplyBox') {
        if (
          !containsCommentId(this.props, this.props.activeReplyBox) &&
          !containsCommentId(nextProps, nextProps.activeReplyBox)
        ) {
          return false;
        }
      }
  
      // Prevent Slot from rerendering when no props has shallowly changed.
      return changes.length !== 0;
    }
  
    static propTypes = {
      // id of currently opened ReplyBox. tracked in Stream.js
      children: PropTypes.node,
      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.object,
      notify: PropTypes.func.isRequired,
      postComment: PropTypes.func.isRequired,
      depth: PropTypes.number.isRequired,
      liveUpdates: PropTypes.bool,
      asset: PropTypes.object.isRequired,
      currentUser: PropTypes.shape({
        id: PropTypes.string.isRequired,
      }),
      charCountEnable: PropTypes.bool.isRequired,
      maxCharCount: PropTypes.number,
      data: PropTypes.object,
      root: PropTypes.object,
      loadMore: PropTypes.func,
      postDontAgree: PropTypes.func,
      animateEnter: PropTypes.bool,
      commentClassNames: PropTypes.array,
      comment: PropTypes.object.isRequired,
      setCommentStatus: PropTypes.func.isRequired,
  
      // edit a comment, passed (id, asset_id, { body })
      editComment: PropTypes.func,
  
      // emit custom events
      emit: PropTypes.func.isRequired,
    };
  
    editComment = (...args) => {
      return this.props.editComment(
        this.props.comment.id,
        this.props.asset.id,
        ...args
      );
    };
  
    onClickEdit(e) {
      e.preventDefault();
      if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) {
        this.props.notify('error', t('error.NOT_AUTHORIZED'));
        return;
      }
      this.setState({ isEditing: true });
    }
  
    stopEditing() {
      if (this._isMounted) {
        this.setState({ isEditing: false });
      }
    }
  
    commentIsRejected(comment) {
      return comment.status === 'REJECTED';
    }
  
    commentIsIgnored(comment) {
      const me = this.props.root.me;
      return (
        me &&
        me.ignoredUsers &&
        me.ignoredUsers.find(u => u.id === comment.user.id)
      );
    }
  
    hasIgnoredReplies() {
      return (
        this.props.comment.replies &&
        this.props.comment.replies.nodes.some(reply =>
          this.commentIsIgnored(reply)
        )
      );
    }
  
    loadNewReplies = () => {
      const { comment: { replies, replyCount, id }, emit } = this.props;
      if (replyCount > replies.nodes.length) {
        this.setState({ loadingState: 'loading' });
        this.props
          .loadMore(id)
          .then(() => {
            this.setState({
              ...resetCursors(this.state, this.props),
              loadingState: 'success',
            });
          })
          .catch(() => {
            this.setState({ loadingState: 'error' });
          });
        emit('ui.Comment.showMoreReplies', { id });
        return;
      }
      this.setState(resetCursors);
      emit('ui.Comment.showMoreReplies', { id });
    };
  
    showReplyBox = () => {
      if (!this.props.currentUser) {
        this.props.showSignInDialog();
        return;
      }
      if (can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) {
        this.props.setActiveReplyBox(this.props.comment.id);
      } else {
        this.props.notify('error', t('error.NOT_AUTHORIZED'));
      }
      return;
    };
  
    commentPostedHandler = () => {
      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() {
      const { comment: { replies }, currentUser, liveUpdates } = this.props;
      const idCursor = this.state.idCursors[0];
      const userId = currentUser ? currentUser.id : null;
  
      if (!replies) {
        return [];
      }
  
      if (liveUpdates) {
        return replies.nodes;
      }
  
      const view = [];
      let pastCursor = false;
      replies.nodes.forEach(comment => {
        if ((idCursor && !pastCursor) || comment.user.id === userId) {
          view.push(comment);
        }
        if (comment.id === idCursor) {
          pastCursor = true;
        }
      });
      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) {
        this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
      }
  
      // if still in the edit window, set a timeout to handle expiration.
      if (this.state.isEditable) {
        const msLeftToEdit = editWindowRemainingMs(this.props.comment);
        this.editWindowExpiryTimeout = setTimeout(() => {
          this.setState({ isEditable: false });
        }, Math.max(msLeftToEdit, 0));
      }
    }
  
    componentWillUnmount() {
      if (this.editWindowExpiryTimeout) {
        this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
      }
      this._isMounted = false;
    }
  
    renderReplyBox() {
      const {
        asset,
        depth,
        comment,
        parentId,
        postComment,
        currentUser,
        setActiveReplyBox,
        maxCharCount,
        notify,
        charCountEnable,
        root,
      } = this.props;
      return (
        <ReplyBox
          root={root}
          comment={comment}
          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,
        depth,
        comment,
        postFlag,
        highlighted,
        postComment,
        currentUser,
        setActiveReplyBox,
        activeReplyBox,
        loadMore,
        deleteAction,
        disableReply,
        maxCharCount,
        notify,
        charCountEnable,
        showSignInDialog,
        liveUpdates,
        emit,
      } = 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>
      );
    }
  
    renderLoadMoreReplies(view) {
      const { comment } = this.props;
      const { loadingState } = this.state;
      const hasMoreComments =
        comment.replies &&
        (comment.replies.hasNextPage ||
          comment.replies.nodes.length > view.length);
      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',
        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);
      }
  
      const commentClassName = cn(
        'talk-stream-comment',
        `talk-stream-comment-level-${depth}`,
        styles.comment,
        styles[`commentLevel${depth}`],
        {
          [styles.pendingComment]: isPending,
          [styles.highlightedComment]: isHighlighted,
          'talk-stream-pending-comment': isPending,
          'talk-stream-highlighted-comment': isHighlighted,
        }
      );
  
      // props that are passed down the slots.
      const slotProps = {
        data,
        depth,
      };
  
      const queryData = {
        root,
        asset,
        comment,
      };
  
      return (
        <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.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
                    fill="commentTimestamp"
                    defaultComponent={CommentTimestamp}
                    className={'talk-stream-comment-published-date'}
                    created_at={comment.created_at}
                    queryData={queryData}
                    {...slotProps}
                  />
                  {comment.editing && comment.editing.edited ? (
                    <span>
                      &nbsp;<span className={styles.editedMarker}>
                        ({t('comment.edited')})
                      </span>
                    </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} />}
            </div>
            <div className={styles.content}>
              {this.state.isEditing ? (
                <EditableCommentContent
                  editComment={this.editComment}
                  notify={notify}
                  root={root}
                  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>
              )}
            </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
  function commentIsStillEditable(comment) {
    const editing = comment && comment.editing;
    if (!editing) {
      return false;
    }
    const editableUntil = getEditableUntilDate(comment);
    const editWindowExpired = editableUntil - new Date() < 0;
    return !editWindowExpired;
  }
  
  // return number of milliseconds before edit window expires
  function editWindowRemainingMs(comment) {
    const editableUntil = getEditableUntilDate(comment);
    if (!editableUntil) {
      return;
    }
    const now = new Date();
    const editWindowRemainingMs = editableUntil - now;
    return editWindowRemainingMs;
  }
  