Comment.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import React, { useEffect, useMemo, useState } from 'react';
  2. import { IUser, pathUtils } from '@growi/core';
  3. import { UserPicture } from '@growi/ui';
  4. import { format, parseISO } from 'date-fns';
  5. import { useTranslation } from 'next-i18next';
  6. import Link from 'next/link';
  7. import { UncontrolledTooltip } from 'reactstrap';
  8. import urljoin from 'url-join';
  9. import { RendererOptions } from '~/services/renderer/renderer';
  10. import { ICommentHasId } from '../../interfaces/comment';
  11. import FormattedDistanceDate from '../FormattedDistanceDate';
  12. import HistoryIcon from '../Icons/HistoryIcon';
  13. import RevisionRenderer from '../Page/RevisionRenderer';
  14. import { Username } from '../User/Username';
  15. import { CommentControl } from './CommentControl';
  16. import { CommentEditor } from './CommentEditor';
  17. import styles from './Comment.module.scss';
  18. type CommentProps = {
  19. comment: ICommentHasId,
  20. rendererOptions: RendererOptions,
  21. revisionId: string,
  22. revisionCreatedAt: Date,
  23. currentUser: IUser,
  24. isReadOnly: boolean,
  25. pageId: string,
  26. pagePath: string,
  27. deleteBtnClicked: (comment: ICommentHasId) => void,
  28. onComment: () => void,
  29. }
  30. export const Comment = (props: CommentProps): JSX.Element => {
  31. const {
  32. comment, rendererOptions, revisionId, revisionCreatedAt, currentUser, isReadOnly,
  33. pageId, pagePath, deleteBtnClicked, onComment,
  34. } = props;
  35. const { returnPathForURL } = pathUtils;
  36. const { t } = useTranslation();
  37. const [markdown, setMarkdown] = useState('');
  38. const [isReEdit, setIsReEdit] = useState(false);
  39. const commentId = comment._id;
  40. const creator = comment.creator;
  41. const isMarkdown = comment.isMarkdown;
  42. const createdAt = new Date(comment.createdAt);
  43. const updatedAt = new Date(comment.updatedAt);
  44. const isEdited = createdAt < updatedAt;
  45. useEffect(() => {
  46. if (revisionId == null) {
  47. return;
  48. }
  49. setMarkdown(comment.comment);
  50. const isCurrentRevision = () => {
  51. return comment.revision === revisionId;
  52. };
  53. isCurrentRevision();
  54. }, [comment, revisionId]);
  55. const isCurrentUserEqualsToAuthor = () => {
  56. const { creator }: any = comment;
  57. if (creator == null || currentUser == null) {
  58. return false;
  59. }
  60. return creator.username === currentUser.username;
  61. };
  62. const getRootClassName = (comment: ICommentHasId) => {
  63. let className = 'page-comment flex-column';
  64. // TODO: fix so that `comment.createdAt` to be type Date https://redmine.weseek.co.jp/issues/113876
  65. let commentCreatedAt = comment.createdAt;
  66. if (typeof commentCreatedAt === 'string') {
  67. commentCreatedAt = parseISO(commentCreatedAt);
  68. }
  69. // Conditional for called from SearchResultContext
  70. if (revisionId != null && revisionCreatedAt != null) {
  71. if (comment.revision === revisionId) {
  72. className += ' page-comment-current';
  73. }
  74. else if (commentCreatedAt.getTime() > revisionCreatedAt.getTime()) {
  75. className += ' page-comment-newer';
  76. }
  77. else {
  78. className += ' page-comment-older';
  79. }
  80. }
  81. if (isCurrentUserEqualsToAuthor()) {
  82. className += ' page-comment-me';
  83. }
  84. return className;
  85. };
  86. const deleteBtnClickedHandler = () => {
  87. deleteBtnClicked(comment);
  88. };
  89. const renderText = (comment: string) => {
  90. return <span style={{ whiteSpace: 'pre-wrap' }}>{comment}</span>;
  91. };
  92. const commentBody = useMemo(() => {
  93. if (rendererOptions == null) {
  94. return <></>;
  95. }
  96. return isMarkdown
  97. ? (
  98. <RevisionRenderer
  99. rendererOptions={rendererOptions}
  100. markdown={markdown}
  101. additionalClassName="comment"
  102. />
  103. )
  104. : renderText(comment.comment);
  105. }, [comment, isMarkdown, markdown, rendererOptions]);
  106. const rootClassName = getRootClassName(comment);
  107. const revHref = `?revisionId=${comment.revision}`;
  108. const editedDateId = `editedDate-${comment._id}`;
  109. const editedDateFormatted = isEdited ? format(updatedAt, 'yyyy/MM/dd HH:mm') : null;
  110. return (
  111. <div className={`${styles['comment-styles']}`}>
  112. { (isReEdit && !isReadOnly) ? (
  113. <CommentEditor
  114. pageId={comment._id}
  115. replyTo={undefined}
  116. currentCommentId={commentId}
  117. commentBody={comment.comment}
  118. onCancelButtonClicked={() => setIsReEdit(false)}
  119. onCommentButtonClicked={() => {
  120. setIsReEdit(false);
  121. if (onComment != null) onComment();
  122. }}
  123. revisionId={revisionId}
  124. />
  125. ) : (
  126. <div id={commentId} className={rootClassName}>
  127. <div className="page-comment-writer">
  128. <UserPicture user={creator} />
  129. </div>
  130. <div className="page-comment-main">
  131. <div className="page-comment-creator">
  132. <Username user={creator} />
  133. </div>
  134. <div className="page-comment-body">{commentBody}</div>
  135. <div className="page-comment-meta">
  136. <Link href={`#${commentId}`} prefetch={false}>
  137. <a>
  138. <FormattedDistanceDate id={commentId} date={comment.createdAt} />
  139. </a>
  140. </Link>
  141. { isEdited && (
  142. <>
  143. <span id={editedDateId}>&nbsp;(edited)</span>
  144. <UncontrolledTooltip placement="bottom" fade={false} target={editedDateId}>{editedDateFormatted}</UncontrolledTooltip>
  145. </>
  146. ) }
  147. <span className="ml-2">
  148. <Link href={urljoin(returnPathForURL(pagePath, pageId), revHref)} prefetch={false}>
  149. <a id={`page-comment-revision-${commentId}`} className="page-comment-revision">
  150. <HistoryIcon />
  151. </a>
  152. </Link>
  153. <UncontrolledTooltip placement="bottom" fade={false} target={`page-comment-revision-${commentId}`}>
  154. {t('page_comment.display_the_page_when_posting_this_comment')}
  155. </UncontrolledTooltip>
  156. </span>
  157. </div>
  158. { (isCurrentUserEqualsToAuthor() && !isReadOnly) && (
  159. <CommentControl
  160. onClickDeleteBtn={deleteBtnClickedHandler}
  161. onClickEditBtn={() => setIsReEdit(true)}
  162. />
  163. ) }
  164. </div>
  165. </div>
  166. ) }
  167. </div>
  168. );
  169. };