Comment.tsx 5.7 KB

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