PageComments.jsx 6.5 KB

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