PageComments.jsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import Button from 'react-bootstrap/es/Button';
  4. import { withTranslation } from 'react-i18next';
  5. import AppContainer from '../services/AppContainer';
  6. import CommentContainer from '../services/CommentContainer';
  7. import PageContainer from '../services/PageContainer';
  8. import { createSubscribedElement } from './UnstatedUtils';
  9. import CommentEditor from './PageComment/CommentEditor';
  10. import Comment from './PageComment/Comment';
  11. import DeleteCommentModal from './PageComment/DeleteCommentModal';
  12. /**
  13. * Load data of comments and render the list of <Comment />
  14. *
  15. * @author Yuki Takei <yuki@weseek.co.jp>
  16. *
  17. * @export
  18. * @class PageComments
  19. * @extends {React.Component}
  20. */
  21. class PageComments extends React.Component {
  22. constructor(props) {
  23. super(props);
  24. this.state = {
  25. // for deleting comment
  26. commentToDelete: undefined,
  27. isDeleteConfirmModalShown: false,
  28. errorMessageForDeleting: undefined,
  29. showEditorIds: new Set(),
  30. };
  31. this.growiRenderer = this.props.appContainer.getRenderer('comment');
  32. this.init = this.init.bind(this);
  33. this.confirmToDeleteComment = this.confirmToDeleteComment.bind(this);
  34. this.deleteComment = this.deleteComment.bind(this);
  35. this.showDeleteConfirmModal = this.showDeleteConfirmModal.bind(this);
  36. this.closeDeleteConfirmModal = this.closeDeleteConfirmModal.bind(this);
  37. this.replyButtonClickedHandler = this.replyButtonClickedHandler.bind(this);
  38. this.commentButtonClickedHandler = this.commentButtonClickedHandler.bind(this);
  39. }
  40. componentWillMount() {
  41. this.init();
  42. }
  43. init() {
  44. if (!this.props.pageContainer.state.pageId) {
  45. return;
  46. }
  47. this.props.commentContainer.retrieveComments();
  48. }
  49. confirmToDeleteComment(comment) {
  50. this.setState({ commentToDelete: comment });
  51. this.showDeleteConfirmModal();
  52. }
  53. deleteComment() {
  54. const comment = this.state.commentToDelete;
  55. this.props.commentContainer.deleteComment(comment)
  56. .then(() => {
  57. this.closeDeleteConfirmModal();
  58. })
  59. .catch((err) => {
  60. this.setState({ errorMessageForDeleting: err.message });
  61. });
  62. }
  63. showDeleteConfirmModal() {
  64. this.setState({ isDeleteConfirmModalShown: true });
  65. }
  66. closeDeleteConfirmModal() {
  67. this.setState({
  68. commentToDelete: undefined,
  69. isDeleteConfirmModalShown: false,
  70. errorMessageForDeleting: undefined,
  71. });
  72. }
  73. replyButtonClickedHandler(commentId) {
  74. const ids = this.state.showEditorIds.add(commentId);
  75. this.setState({ showEditorIds: ids });
  76. }
  77. commentButtonClickedHandler(commentId) {
  78. this.setState((prevState) => {
  79. prevState.showEditorIds.delete(commentId);
  80. return {
  81. showEditorIds: prevState.showEditorIds,
  82. };
  83. });
  84. }
  85. // get replies to specific comment object
  86. getRepliesFor(comment, allReplies) {
  87. const replyList = [];
  88. allReplies.forEach((reply) => {
  89. if (reply.replyTo === comment._id) {
  90. replyList.push(reply);
  91. }
  92. });
  93. return replyList;
  94. }
  95. /**
  96. * render Elements of Comment Thread
  97. *
  98. * @param {any} comment Comment Model Obj
  99. * @param {any} replies List of Reply Comment Model Obj
  100. *
  101. * @memberOf PageComments
  102. */
  103. renderThread(comment, replies) {
  104. const commentId = comment._id;
  105. const showEditor = this.state.showEditorIds.has(commentId);
  106. const isLoggedIn = this.props.appContainer.me != null;
  107. let rootClassNames = 'page-comment-thread';
  108. if (replies.length === 0) {
  109. rootClassNames += ' page-comment-thread-no-replies';
  110. }
  111. return (
  112. <div key={commentId} className={`mb-5 ${rootClassNames}`}>
  113. <Comment
  114. comment={comment}
  115. editBtnClicked={this.confirmToEditComment}
  116. deleteBtnClicked={this.confirmToDeleteComment}
  117. growiRenderer={this.growiRenderer}
  118. replyList={replies}
  119. />
  120. { !showEditor && isLoggedIn && (
  121. <div className="text-right">
  122. <Button
  123. bsStyle="default"
  124. className="btn btn-outline btn-default btn-sm btn-comment-reply"
  125. onClick={() => { return this.replyButtonClickedHandler(commentId) }}
  126. >
  127. <i className="icon-fw icon-action-redo"></i> Reply
  128. </Button>
  129. </div>
  130. )}
  131. { showEditor && isLoggedIn && (
  132. <div className="page-comment-reply-form">
  133. <CommentEditor
  134. growiRenderer={this.growiRenderer}
  135. replyTo={commentId}
  136. commentButtonClickedHandler={this.commentButtonClickedHandler}
  137. />
  138. </div>
  139. )}
  140. </div>
  141. );
  142. }
  143. render() {
  144. const topLevelComments = [];
  145. const allReplies = [];
  146. const layoutType = this.props.appContainer.getConfig().layoutType;
  147. const isBaloonStyle = layoutType.match(/crowi-plus|growi|kibela/);
  148. let comments = this.props.commentContainer.state.comments;
  149. if (isBaloonStyle) {
  150. // replace with asc order array
  151. comments = comments.slice().reverse(); // non-destructive reverse
  152. }
  153. comments.forEach((comment) => {
  154. if (comment.replyTo === undefined) {
  155. // comment is not a reply
  156. topLevelComments.push(comment);
  157. }
  158. else {
  159. // comment is a reply
  160. allReplies.push(comment);
  161. }
  162. });
  163. return (
  164. <div>
  165. { topLevelComments.map((topLevelComment) => {
  166. // get related replies
  167. const replies = this.getRepliesFor(topLevelComment, allReplies);
  168. return this.renderThread(topLevelComment, replies);
  169. }) }
  170. <DeleteCommentModal
  171. isShown={this.state.isDeleteConfirmModalShown}
  172. comment={this.state.commentToDelete}
  173. errorMessage={this.state.errorMessageForDeleting}
  174. cancel={this.closeDeleteConfirmModal}
  175. confirmedToDelete={this.deleteComment}
  176. />
  177. </div>
  178. );
  179. }
  180. }
  181. /**
  182. * Wrapper component for using unstated
  183. */
  184. const PageCommentsWrapper = (props) => {
  185. return createSubscribedElement(PageComments, props, [AppContainer, PageContainer, CommentContainer]);
  186. };
  187. PageComments.propTypes = {
  188. appContainer: PropTypes.instanceOf(AppContainer).isRequired,
  189. pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
  190. commentContainer: PropTypes.instanceOf(CommentContainer).isRequired,
  191. };
  192. export default withTranslation()(PageCommentsWrapper);