PageComments.jsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  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 { Subscribe } from 'unstated';
  6. import { withTranslation } from 'react-i18next';
  7. import GrowiRenderer from '../util/GrowiRenderer';
  8. import CommentContainer from './PageComment/CommentContainer';
  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. isLayoutTypeGrowi: false,
  26. // for deleting comment
  27. commentToDelete: undefined,
  28. isDeleteConfirmModalShown: false,
  29. errorMessageForDeleting: undefined,
  30. showEditorIds: new Set(),
  31. };
  32. this.growiRenderer = new GrowiRenderer(this.props.crowi, this.props.crowiOriginRenderer, { mode: '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. }
  40. componentWillMount() {
  41. this.init();
  42. }
  43. init() {
  44. if (!this.props.pageId) {
  45. return;
  46. }
  47. const layoutType = this.props.crowi.getConfig().layoutType;
  48. this.setState({ isLayoutTypeGrowi: layoutType === 'crowi-plus' || layoutType === 'growi' });
  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. // adds replies to specific comment object
  80. addRepliesToComments(comments, replies) {
  81. const commentsWithReplies = [];
  82. const commentsCopy = comments.slice();
  83. commentsCopy.forEach((comment) => {
  84. comment.replyList = [];
  85. replies.forEach((reply) => {
  86. if (reply.replyTo === comment._id) {
  87. comment.replyList.push(reply);
  88. }
  89. });
  90. commentsWithReplies.push(comment);
  91. });
  92. return commentsWithReplies;
  93. }
  94. // returns replies
  95. renderReplies(comment) {
  96. return comment.replyList.map((reply) => {
  97. return (
  98. <div key={reply._id} 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">
  99. <Comment
  100. comment={reply}
  101. deleteBtnClicked={this.confirmToDeleteComment}
  102. crowiRenderer={this.growiRenderer}
  103. onReplyButtonClicked={() => { this.replyButtonClickedHandler(reply._id) }}
  104. crowi={this.props.crowi}
  105. replyTo={comment._id}
  106. />
  107. </div>
  108. );
  109. });
  110. }
  111. /**
  112. * generate Elements of Comment
  113. *
  114. * @param {any} comments Array of Comment Model Obj
  115. *
  116. * @memberOf PageComments
  117. */
  118. generateCommentElements(comments, replies) {
  119. const commentsWithReplies = this.addRepliesToComments(comments, replies);
  120. return commentsWithReplies.map((comment) => {
  121. const commentId = comment._id;
  122. const showEditor = this.state.showEditorIds.has(commentId);
  123. return (
  124. <div key={commentId}>
  125. <Comment
  126. comment={comment}
  127. deleteBtnClicked={this.confirmToDeleteComment}
  128. crowiRenderer={this.growiRenderer}
  129. onReplyButtonClicked={() => { this.replyButtonClickedHandler(commentId) }}
  130. crowi={this.props.crowi}
  131. replyTo={undefined}
  132. />
  133. <div className="container-fluid">
  134. <div className="row">
  135. {this.renderReplies(comment)}
  136. <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">
  137. { showEditor && (
  138. <CommentEditor
  139. crowi={this.props.crowi}
  140. crowiOriginRenderer={this.props.crowiOriginRenderer}
  141. editorOptions={this.props.editorOptions}
  142. slackChannels={this.props.slackChannels}
  143. replyTo={commentId}
  144. />
  145. )}
  146. </div>
  147. </div>
  148. </div>
  149. </div>
  150. );
  151. });
  152. }
  153. render() {
  154. const currentComments = [];
  155. const newerComments = [];
  156. const olderComments = [];
  157. const currentReplies = [];
  158. const newerReplies = [];
  159. const olderReplies = [];
  160. let comments = this.props.commentContainer.state.comments;
  161. if (this.state.isLayoutTypeGrowi) {
  162. // replace with asc order array
  163. comments = comments.slice().reverse(); // non-destructive reverse
  164. }
  165. // divide by revisionId and createdAt
  166. const revisionId = this.props.revisionId;
  167. const revisionCreatedAt = this.props.revisionCreatedAt;
  168. comments.forEach((comment) => {
  169. // comparing ObjectId
  170. // eslint-disable-next-line eqeqeq
  171. if (comment.replyTo === undefined) {
  172. // comment is not a reply
  173. if (comment.revision === revisionId) {
  174. currentComments.push(comment);
  175. }
  176. else if (Date.parse(comment.createdAt) / 1000 > revisionCreatedAt) {
  177. newerComments.push(comment);
  178. }
  179. else {
  180. olderComments.push(comment);
  181. }
  182. }
  183. else
  184. // comment is a reply
  185. if (comment.revision === revisionId) {
  186. currentReplies.push(comment);
  187. }
  188. else if (Date.parse(comment.createdAt) / 1000 > revisionCreatedAt) {
  189. newerReplies.push(comment);
  190. }
  191. else {
  192. olderReplies.push(comment);
  193. }
  194. });
  195. // generate elements
  196. const currentElements = this.generateCommentElements(currentComments, currentReplies);
  197. const newerElements = this.generateCommentElements(newerComments, newerReplies);
  198. const olderElements = this.generateCommentElements(olderComments, olderReplies);
  199. // generate blocks
  200. const currentBlock = (
  201. <div className="page-comments-list-current" id="page-comments-list-current">
  202. {currentElements}
  203. </div>
  204. );
  205. const newerBlock = (
  206. <div className="page-comments-list-newer collapse in" id="page-comments-list-newer">
  207. {newerElements}
  208. </div>
  209. );
  210. const olderBlock = (
  211. <div className="page-comments-list-older collapse in" id="page-comments-list-older">
  212. {olderElements}
  213. </div>
  214. );
  215. // generate toggle elements
  216. const iconForNewer = (this.state.isLayoutTypeGrowi)
  217. ? <i className="fa fa-angle-double-down"></i>
  218. : <i className="fa fa-angle-double-up"></i>;
  219. const toggleNewer = (newerElements.length === 0)
  220. ? <div></div>
  221. : (
  222. <a className="page-comments-list-toggle-newer text-center" data-toggle="collapse" href="#page-comments-list-newer">
  223. {iconForNewer} Comments for Newer Revision {iconForNewer}
  224. </a>
  225. );
  226. const iconForOlder = (this.state.isLayoutTypeGrowi)
  227. ? <i className="fa fa-angle-double-up"></i>
  228. : <i className="fa fa-angle-double-down"></i>;
  229. const toggleOlder = (olderElements.length === 0)
  230. ? <div></div>
  231. : (
  232. <a className="page-comments-list-toggle-older text-center" data-toggle="collapse" href="#page-comments-list-older">
  233. {iconForOlder} Comments for Older Revision {iconForOlder}
  234. </a>
  235. );
  236. // layout blocks
  237. const commentsElements = (this.state.isLayoutTypeGrowi)
  238. ? (
  239. <div>
  240. {olderBlock}
  241. {toggleOlder}
  242. {currentBlock}
  243. {toggleNewer}
  244. {newerBlock}
  245. </div>
  246. )
  247. : (
  248. <div>
  249. {newerBlock}
  250. {toggleNewer}
  251. {currentBlock}
  252. {toggleOlder}
  253. {olderBlock}
  254. </div>
  255. );
  256. return (
  257. <div>
  258. {commentsElements}
  259. <DeleteCommentModal
  260. isShown={this.state.isDeleteConfirmModalShown}
  261. comment={this.state.commentToDelete}
  262. errorMessage={this.state.errorMessageForDeleting}
  263. cancel={this.closeDeleteConfirmModal}
  264. confirmedToDelete={this.deleteComment}
  265. />
  266. </div>
  267. );
  268. }
  269. }
  270. /**
  271. * Wrapper component for using unstated
  272. */
  273. class PageCommentsWrapper extends React.Component {
  274. render() {
  275. return (
  276. <Subscribe to={[CommentContainer]}>
  277. { commentContainer => (
  278. // eslint-disable-next-line arrow-body-style
  279. <PageComments commentContainer={commentContainer} {...this.props} />
  280. )}
  281. </Subscribe>
  282. );
  283. }
  284. }
  285. PageCommentsWrapper.propTypes = {
  286. crowi: PropTypes.object.isRequired,
  287. crowiOriginRenderer: PropTypes.object.isRequired,
  288. pageId: PropTypes.string.isRequired,
  289. revisionId: PropTypes.string.isRequired,
  290. revisionCreatedAt: PropTypes.number,
  291. pagePath: PropTypes.string,
  292. editorOptions: PropTypes.object,
  293. slackChannels: PropTypes.string,
  294. };
  295. PageComments.propTypes = {
  296. commentContainer: PropTypes.object.isRequired,
  297. crowi: PropTypes.object.isRequired,
  298. crowiOriginRenderer: PropTypes.object.isRequired,
  299. pageId: PropTypes.string.isRequired,
  300. revisionId: PropTypes.string.isRequired,
  301. revisionCreatedAt: PropTypes.number,
  302. pagePath: PropTypes.string,
  303. editorOptions: PropTypes.object,
  304. slackChannels: PropTypes.string,
  305. };
  306. export default withTranslation(null, { withRef: true })(PageCommentsWrapper);