Comment.tsx 6.4 KB

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