PageComments.jsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. /* eslint-disable react/no-multi-comp */
  2. /* eslint-disable react/no-access-state-in-setstate */
  3. import React from 'react';
  4. import PropTypes from 'prop-types';
  5. import Button from 'react-bootstrap/es/Button';
  6. import { Subscribe } from 'unstated';
  7. import { withTranslation } from 'react-i18next';
  8. import GrowiRenderer from '../util/GrowiRenderer';
  9. import AppContainer from '../services/AppContainer';
  10. import CommentContainer from './PageComment/CommentContainer';
  11. import CommentEditor from './PageComment/CommentEditor';
  12. import Comment from './PageComment/Comment';
  13. import DeleteCommentModal from './PageComment/DeleteCommentModal';
  14. import PageContainer from '../services/PageContainer';
  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. isLayoutTypeGrowi: false,
  29. // for deleting comment
  30. commentToDelete: undefined,
  31. isDeleteConfirmModalShown: false,
  32. errorMessageForDeleting: undefined,
  33. showEditorIds: new Set(),
  34. };
  35. this.growiRenderer = new GrowiRenderer(window.crowi, this.props.crowiOriginRenderer, { mode: 'comment' });
  36. this.init = this.init.bind(this);
  37. this.confirmToDeleteComment = this.confirmToDeleteComment.bind(this);
  38. this.deleteComment = this.deleteComment.bind(this);
  39. this.showDeleteConfirmModal = this.showDeleteConfirmModal.bind(this);
  40. this.closeDeleteConfirmModal = this.closeDeleteConfirmModal.bind(this);
  41. this.replyButtonClickedHandler = this.replyButtonClickedHandler.bind(this);
  42. this.commentButtonClickedHandler = this.commentButtonClickedHandler.bind(this);
  43. }
  44. componentWillMount() {
  45. this.init();
  46. }
  47. init() {
  48. if (!this.props.pageContainer.state.pageId) {
  49. return;
  50. }
  51. const layoutType = this.props.appContainer.getConfig().layoutType;
  52. this.setState({ isLayoutTypeGrowi: layoutType === 'crowi-plus' || layoutType === 'growi' });
  53. this.props.commentContainer.retrieveComments();
  54. }
  55. confirmToDeleteComment(comment) {
  56. this.setState({ commentToDelete: comment });
  57. this.showDeleteConfirmModal();
  58. }
  59. deleteComment() {
  60. const comment = this.state.commentToDelete;
  61. this.props.commentContainer.deleteComment(comment)
  62. .then(() => {
  63. this.closeDeleteConfirmModal();
  64. })
  65. .catch((err) => {
  66. this.setState({ errorMessageForDeleting: err.message });
  67. });
  68. }
  69. showDeleteConfirmModal() {
  70. this.setState({ isDeleteConfirmModalShown: true });
  71. }
  72. closeDeleteConfirmModal() {
  73. this.setState({
  74. commentToDelete: undefined,
  75. isDeleteConfirmModalShown: false,
  76. errorMessageForDeleting: undefined,
  77. });
  78. }
  79. replyButtonClickedHandler(commentId) {
  80. const ids = this.state.showEditorIds.add(commentId);
  81. this.setState({ showEditorIds: ids });
  82. }
  83. commentButtonClickedHandler(commentId) {
  84. this.setState((prevState) => {
  85. prevState.showEditorIds.delete(commentId);
  86. return {
  87. showEditorIds: prevState.showEditorIds,
  88. };
  89. });
  90. }
  91. // adds replies to specific comment object
  92. addRepliesToComments(comment, replies) {
  93. const replyList = [];
  94. replies.forEach((reply) => {
  95. if (reply.replyTo === comment._id) {
  96. replyList.push(reply);
  97. }
  98. });
  99. return replyList;
  100. }
  101. /**
  102. * generate Elements of Comment
  103. *
  104. * @param {any} comments Array of Comment Model Obj
  105. *
  106. * @memberOf PageComments
  107. */
  108. generateCommentElements(comments, replies) {
  109. return comments.map((comment) => {
  110. const commentId = comment._id;
  111. const showEditor = this.state.showEditorIds.has(commentId);
  112. const username = this.props.appContainer.me;
  113. const replyList = this.addRepliesToComments(comment, replies);
  114. return (
  115. <div key={commentId}>
  116. <Comment
  117. comment={comment}
  118. deleteBtnClicked={this.confirmToDeleteComment}
  119. crowiRenderer={this.growiRenderer}
  120. replyList={replyList}
  121. revisionCreatedAt={this.props.revisionCreatedAt}
  122. />
  123. <div className="container-fluid">
  124. <div className="row">
  125. <div className="col-xs-offset-1 col-xs-11 col-sm-offset-1 col-sm-11 col-md-offset-1 col-md-11 col-lg-offset-1 col-lg-11">
  126. { !showEditor && (
  127. <div>
  128. { username
  129. && (
  130. <div className="col-xs-offset-6 col-sm-offset-6 col-md-offset-6 col-lg-offset-6">
  131. <Button
  132. bsStyle="primary"
  133. className="fcbtn btn btn-sm btn-primary btn-outline btn-rounded btn-1b"
  134. onClick={() => { return this.replyButtonClickedHandler(commentId) }}
  135. >
  136. <i className="icon-bubble"></i> Reply
  137. </Button>
  138. </div>
  139. )
  140. }
  141. </div>
  142. )}
  143. { showEditor && (
  144. <CommentEditor
  145. crowiOriginRenderer={this.props.crowiOriginRenderer}
  146. slackChannels={this.props.slackChannels}
  147. replyTo={commentId}
  148. commentButtonClickedHandler={this.commentButtonClickedHandler}
  149. />
  150. )}
  151. </div>
  152. </div>
  153. </div>
  154. <br />
  155. </div>
  156. );
  157. });
  158. }
  159. render() {
  160. const currentComments = [];
  161. const currentReplies = [];
  162. let comments = this.props.commentContainer.state.comments;
  163. if (this.state.isLayoutTypeGrowi) {
  164. // replace with asc order array
  165. comments = comments.slice().reverse(); // non-destructive reverse
  166. }
  167. comments.forEach((comment) => {
  168. if (comment.replyTo === undefined) {
  169. // comment is not a reply
  170. currentComments.push(comment);
  171. }
  172. else {
  173. // comment is a reply
  174. currentReplies.push(comment);
  175. }
  176. });
  177. // generate elements
  178. const currentElements = this.generateCommentElements(currentComments, currentReplies);
  179. // generate blocks
  180. const currentBlock = (
  181. <div className="page-comments-list-current" id="page-comments-list-current">
  182. {currentElements}
  183. </div>
  184. );
  185. // layout blocks
  186. const commentsElements = (<div>{currentBlock}</div>);
  187. return (
  188. <div>
  189. {commentsElements}
  190. <DeleteCommentModal
  191. isShown={this.state.isDeleteConfirmModalShown}
  192. comment={this.state.commentToDelete}
  193. errorMessage={this.state.errorMessageForDeleting}
  194. cancel={this.closeDeleteConfirmModal}
  195. confirmedToDelete={this.deleteComment}
  196. />
  197. </div>
  198. );
  199. }
  200. }
  201. /**
  202. * Wrapper component for using unstated
  203. */
  204. class PageCommentsWrapper extends React.Component {
  205. render() {
  206. return (
  207. <Subscribe to={[AppContainer, PageContainer, CommentContainer]}>
  208. { (appContainer, pageContainer, commentContainer) => (
  209. // eslint-disable-next-line arrow-body-style
  210. <PageComments appContainer={appContainer} pageContainer={pageContainer} commentContainer={commentContainer} {...this.props} />
  211. )}
  212. </Subscribe>
  213. );
  214. }
  215. }
  216. PageComments.propTypes = {
  217. appContainer: PropTypes.instanceOf(AppContainer).isRequired,
  218. pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
  219. commentContainer: PropTypes.instanceOf(CommentContainer).isRequired,
  220. crowiOriginRenderer: PropTypes.object.isRequired,
  221. revisionCreatedAt: PropTypes.number,
  222. slackChannels: PropTypes.string,
  223. };
  224. PageCommentsWrapper.propTypes = {
  225. crowiOriginRenderer: PropTypes.object.isRequired,
  226. revisionCreatedAt: PropTypes.number,
  227. slackChannels: PropTypes.string,
  228. };
  229. export default withTranslation(null, { withRef: true })(PageCommentsWrapper);