Comment.tsx 5.7 KB

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