PageComments.jsx 6.3 KB

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