PageComments.jsx 6.5 KB

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