2
0
Эх сурвалжийг харах

Merge pull request #5609 from weseek/imprv/90378-91010-share-page-comment-component

imprv: share page comment component
Yuki Takei 4 жил өмнө
parent
commit
2571f8cf8f

+ 3 - 3
packages/app/src/client/app.jsx

@@ -19,7 +19,7 @@ import DisplaySwitcher from '../components/Page/DisplaySwitcher';
 import { defaultEditorOptions, defaultPreviewOptions } from '../components/PageEditor/OptionsSelector';
 import Page from '../components/Page';
 import PageContentFooter from '../components/PageContentFooter';
-import PageCommentList from '../components/PageCommentList';
+import PageComment from '../components/PageComment';
 import PageTimeline from '../components/PageTimeline';
 import CommentEditorLazyRenderer from '../components/PageComment/CommentEditorLazyRenderer';
 import ShareLinkAlert from '../components/Page/ShareLinkAlert';
@@ -120,8 +120,8 @@ Object.assign(componentMappings, {
 // additional definitions if data exists
 if (pageContainer.state.pageId != null) {
   Object.assign(componentMappings, {
-    'page-comments-list': <PageCommentList appContainer={appContainer} pageId={pageContainer.state.pageId} />,
-    'page-comment-write': <CommentEditorLazyRenderer appContainer={appContainer} />,
+    'page-comments-list': <PageComment appContainer={appContainer} pageId={pageContainer.state.pageId} isReadOnly={false} titleAlign="left" />,
+    'page-comment-write': <CommentEditorLazyRenderer appContainer={appContainer} pageId={pageContainer.state.pageId} />,
     'page-content-footer': <PageContentFooter
       createdAt={new Date(pageContainer.state.createdAt)}
       updatedAt={new Date(pageContainer.state.updatedAt)}

+ 219 - 0
packages/app/src/components/PageComment.tsx

@@ -0,0 +1,219 @@
+import React, {
+  FC, useEffect, useState, useMemo, memo, useCallback,
+} from 'react';
+
+import { Button } from 'reactstrap';
+
+import CommentEditor from './PageComment/CommentEditor';
+import Comment from './PageComment/Comment';
+import ReplayComments from './PageComment/ReplayComments';
+import DeleteCommentModal from './PageComment/DeleteCommentModal';
+
+import AppContainer from '~/client/services/AppContainer';
+import { toastError } from '~/client/util/apiNotification';
+
+import { useSWRxPageComment } from '../stores/comment';
+
+import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
+
+type Props = {
+  appContainer: AppContainer,
+  pageId: string,
+  isReadOnly : boolean,
+  titleAlign?: 'center' | 'left' | 'right',
+  highlightKeywords?:string[],
+  hideIfEmpty?: boolean,
+}
+
+
+const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
+
+  const {
+    appContainer, pageId, highlightKeywords, isReadOnly, titleAlign, hideIfEmpty,
+  } = props;
+
+  const { data: comments, mutate } = useSWRxPageComment(pageId);
+
+  const [commentToBeDeleted, setCommentToBeDeleted] = useState<ICommentHasId | null>(null);
+  const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState<boolean>(false);
+  const [showEditorIds, setShowEditorIds] = useState<Set<string>>(new Set());
+  const [formatedComments, setFormatedComments] = useState<ICommentHasIdList | null>(null);
+  const [errorMessageOnDelete, setErrorMessageOnDelete] = useState<string>('');
+
+  const commentsFromOldest = useMemo(() => (formatedComments != null ? [...formatedComments].reverse() : null), [formatedComments]);
+  const commentsExceptReply: ICommentHasIdList | undefined = useMemo(
+    () => commentsFromOldest?.filter(comment => comment.replyTo == null), [commentsFromOldest],
+  );
+  const allReplies = {};
+
+  const highlightComment = useCallback((comment: string):string => {
+    if (highlightKeywords == null) return comment;
+
+    let highlightedComment = '';
+    highlightKeywords.forEach((highlightKeyword) => {
+      highlightedComment = comment.replaceAll(highlightKeyword, '<em class="highlighted-keyword">$&</em>');
+    });
+    return highlightedComment;
+  }, [highlightKeywords]);
+
+  useEffect(() => {
+
+    if (comments != null) {
+      const preprocessedCommentList: string[] = comments.map((comment) => {
+        const highlightedComment: string = highlightComment(comment.comment);
+        return highlightedComment;
+      });
+      const preprocessedComments: ICommentHasIdList = comments.map((comment, index) => {
+        return { ...comment, comment: preprocessedCommentList[index] };
+      });
+      setFormatedComments(preprocessedComments);
+    }
+
+  }, [comments, highlightComment]);
+
+  if (commentsFromOldest != null) {
+    commentsFromOldest.forEach((comment) => {
+      if (comment.replyTo != null) {
+        allReplies[comment.replyTo] = allReplies[comment.replyTo] == null ? [comment] : [...allReplies[comment.replyTo], comment];
+      }
+    });
+  }
+
+  const onClickDeleteButton = useCallback((comment: ICommentHasId) => {
+    setCommentToBeDeleted(comment);
+    setIsDeleteConfirmModalShown(true);
+  }, []);
+
+  const onCancelDeleteComment = useCallback(() => {
+    setCommentToBeDeleted(null);
+    setIsDeleteConfirmModalShown(false);
+  }, []);
+
+  const onDeleteCommentAfterOperation = useCallback(() => {
+    onCancelDeleteComment();
+    mutate();
+  }, [mutate, onCancelDeleteComment]);
+
+  const onDeleteComment = useCallback(async() => {
+    if (commentToBeDeleted == null) return;
+    try {
+      await appContainer.apiPost('/comments.remove', { comment_id: commentToBeDeleted._id });
+      onDeleteCommentAfterOperation();
+    }
+    catch (error:unknown) {
+      setErrorMessageOnDelete(error as string);
+      toastError(`error: ${error}`);
+    }
+  }, [appContainer, commentToBeDeleted, onDeleteCommentAfterOperation]);
+
+  const generateCommentInnerElement = (comment: ICommentHasId) => (
+    <Comment
+      growiRenderer={appContainer.getRenderer('comment')}
+      deleteBtnClicked={onClickDeleteButton}
+      comment={comment}
+      onComment={mutate}
+      isReadOnly={isReadOnly}
+    />
+  );
+
+  const generateAllRepliesElement = (replyComments: ICommentHasIdList) => (
+    <ReplayComments
+      replyList={replyComments}
+      deleteBtnClicked={onClickDeleteButton}
+      growiRenderer={appContainer.getRenderer('comment')}
+      isReadOnly={isReadOnly}
+    />
+  );
+
+  const removeShowEditorId = useCallback((commentId: string) => {
+    setShowEditorIds((previousState) => {
+      const previousShowEditorIds = new Set(...previousState);
+      previousShowEditorIds.delete(commentId);
+      return previousShowEditorIds;
+    });
+  }, []);
+
+
+  if (commentsFromOldest == null || commentsExceptReply == null) return <></>;
+
+  if (hideIfEmpty && comments?.length === 0) {
+    return <></>;
+  }
+
+  let commentTitleClasses = 'border-bottom py-3 mb-3';
+  commentTitleClasses = titleAlign != null ? `${commentTitleClasses} text-${titleAlign}` : `${commentTitleClasses} text-center`;
+
+  return (
+    <>
+      <div className="page-comments-row comment-list">
+        <div className="container-lg">
+          <div className="page-comments">
+            <h2 className={commentTitleClasses}><i className="icon-fw icon-bubbles"></i>Comments</h2>
+            <div className="page-comments-list" id="page-comments-list">
+              { commentsExceptReply.map((comment) => {
+
+                const defaultCommentThreadClasses = 'page-comment-thread pb-5';
+                const hasReply: boolean = Object.keys(allReplies).includes(comment._id);
+
+                let commentThreadClasses = '';
+                commentThreadClasses = hasReply ? `${defaultCommentThreadClasses} page-comment-thread-no-replies` : defaultCommentThreadClasses;
+
+                return (
+                  <div key={comment._id} className={commentThreadClasses}>
+                    {/* display comment */}
+                    {generateCommentInnerElement(comment)}
+                    {/* display reply comment */}
+                    {hasReply && generateAllRepliesElement(allReplies[comment._id])}
+                    {/* display reply button */}
+                    {(!isReadOnly && !showEditorIds.has(comment._id)) && (
+                      <div className="text-right">
+                        <Button
+                          outline
+                          color="secondary"
+                          size="sm"
+                          className="btn-comment-reply"
+                          onClick={() => {
+                            setShowEditorIds(previousState => new Set(previousState.add(comment._id)));
+                          }}
+                        >
+                          <i className="icon-fw icon-action-undo"></i> Reply
+                        </Button>
+                      </div>
+                    )}
+                    {/* display reply editor */}
+                    {(!isReadOnly && showEditorIds.has(comment._id)) && (
+                      <CommentEditor
+                        growiRenderer={appContainer.getRenderer('comment')}
+                        replyTo={comment._id}
+                        onCancelButtonClicked={() => {
+                          removeShowEditorId(comment._id);
+                        }}
+                        onCommentButtonClicked={() => {
+                          removeShowEditorId(comment._id);
+                          mutate();
+                        }}
+                      />
+                    )}
+                  </div>
+                );
+
+              })}
+            </div>
+          </div>
+        </div>
+      </div>
+      {(!isReadOnly && commentToBeDeleted != null) && (
+        <DeleteCommentModal
+          isShown={isDeleteConfirmModalShown}
+          comment={commentToBeDeleted}
+          errorMessage={errorMessageOnDelete}
+          cancel={onCancelDeleteComment}
+          confirmedToDelete={onDeleteComment}
+        />
+      )}
+    </>
+  );
+});
+
+
+export default PageComment;

+ 23 - 12
packages/app/src/components/PageComment/Comment.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import { format } from 'date-fns';
 
 import { UncontrolledTooltip } from 'reactstrap';
@@ -147,8 +147,9 @@ class Comment extends React.PureComponent {
   }
 
   render() {
-    const { t } = this.props;
-    const comment = this.props.comment;
+    const {
+      t, comment, isReadOnly, onComment,
+    } = this.props;
     const commentId = comment._id;
     const creator = comment.creator;
     const isMarkdown = comment.isMarkdown;
@@ -167,7 +168,7 @@ class Comment extends React.PureComponent {
 
     return (
       <React.Fragment>
-        {this.state.isReEdit ? (
+        {(this.state.isReEdit && !isReadOnly) ? (
           <CommentEditor
             growiRenderer={this.props.growiRenderer}
             currentCommentId={commentId}
@@ -175,7 +176,10 @@ class Comment extends React.PureComponent {
             replyTo={undefined}
             commentCreator={creator?.username}
             onCancelButtonClicked={() => this.setState({ isReEdit: false })}
-            onCommentButtonClicked={() => this.setState({ isReEdit: false })}
+            onCommentButtonClicked={() => {
+              this.setState({ isReEdit: false });
+              if (onComment != null) onComment();
+            }}
           />
         ) : (
           <div id={commentId} className={rootClassName}>
@@ -206,7 +210,7 @@ class Comment extends React.PureComponent {
                   </UncontrolledTooltip>
                 </span>
               </div>
-              {this.isCurrentUserEqualsToAuthor() && (
+              {(this.isCurrentUserEqualsToAuthor() && !isReadOnly) && (
                 <CommentControl
                   onClickDeleteBtn={this.deleteBtnClickedHandler}
                   onClickEditBtn={() => this.setState({ isReEdit: true })}
@@ -222,19 +226,26 @@ class Comment extends React.PureComponent {
 
 }
 
-/**
- * Wrapper component for using unstated
- */
-const CommentWrapper = withUnstatedContainers(Comment, [AppContainer, PageContainer]);
-
 Comment.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
   comment: PropTypes.object.isRequired,
+  isReadOnly: PropTypes.bool.isRequired,
   growiRenderer: PropTypes.object.isRequired,
   deleteBtnClicked: PropTypes.func.isRequired,
+  onComment: PropTypes.func,
+};
+
+const CommentWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <Comment t={t} {...props} />;
 };
 
-export default withTranslation()(CommentWrapper);
+/**
+ * Wrapper component for using unstated
+ */
+const CommentWrapper = withUnstatedContainers(CommentWrapperFC, [AppContainer, PageContainer]);
+
+export default CommentWrapper;

+ 1 - 1
packages/app/src/components/PageComment/CommentEditor.jsx

@@ -171,7 +171,7 @@ class CommentEditor extends React.Component {
       this.initializeEditor();
 
       if (onCommentButtonClicked != null) {
-        onCommentButtonClicked(replyTo || currentCommentId);
+        onCommentButtonClicked();
       }
     }
     catch (err) {

+ 7 - 0
packages/app/src/components/PageComment/CommentEditorLazyRenderer.tsx

@@ -1,15 +1,21 @@
 import React, { FC } from 'react';
 
+import { useSWRxPageComment } from '../../stores/comment';
+
 import AppContainer from '~/client/services/AppContainer';
 
 import CommentEditor from './CommentEditor';
 
 type Props = {
   appContainer: AppContainer,
+  pageId: string,
 }
 
 const CommentEditorLazyRenderer:FC<Props> = (props:Props):JSX.Element => {
 
+  const { pageId } = props;
+  const { mutate } = useSWRxPageComment(pageId);
+
   const { appContainer } = props;
   const growiRenderer = appContainer.getRenderer('comment');
 
@@ -18,6 +24,7 @@ const CommentEditorLazyRenderer:FC<Props> = (props:Props):JSX.Element => {
       appContainer={appContainer}
       growiRenderer={growiRenderer}
       replyTo={undefined}
+      onCommentButtonClicked={mutate}
       isForNewComment
     />
   );

+ 2 - 0
packages/app/src/components/PageComment/ReplayComments.jsx

@@ -33,6 +33,7 @@ class ReplayComments extends React.PureComponent {
           comment={reply}
           deleteBtnClicked={this.props.deleteBtnClicked}
           growiRenderer={this.props.growiRenderer}
+          isReadOnly={this.props.isReadOnly}
         />
       </div>
     );
@@ -108,6 +109,7 @@ ReplayComments.propTypes = {
 
   growiRenderer: PropTypes.object.isRequired,
   deleteBtnClicked: PropTypes.func.isRequired,
+  isReadOnly: PropTypes.bool.isRequired,
   replyList: PropTypes.array,
 };
 

+ 0 - 218
packages/app/src/components/PageCommentList.tsx

@@ -1,218 +0,0 @@
-import React, {
-  FC, useEffect, useState, useMemo, memo, useCallback,
-} from 'react';
-
-import { UncontrolledTooltip } from 'reactstrap';
-import { useTranslation } from 'react-i18next';
-
-import { UserPicture } from '@growi/ui';
-import AppContainer from '~/client/services/AppContainer';
-
-import RevisionBody from './Page/RevisionBody';
-import Username from './User/Username';
-import FormattedDistanceDate from './FormattedDistanceDate';
-import HistoryIcon from './Icons/HistoryIcon';
-
-import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
-
-import { useSWRxPageComment } from '../stores/comment';
-
-import MathJaxConfigurer from '~/client/util/markdown-it/mathjax';
-
-const COMMENT_BOTTOM_MARGIN = 'mb-5';
-
-type Props = {
-  appContainer: AppContainer,
-  pageId: string,
-  highlightKeywords?:string[],
-}
-
-// todo: update this component to shared PageComment component
-const PageCommentList:FC<Props> = memo((props:Props):JSX.Element => {
-
-  const { appContainer, pageId, highlightKeywords } = props;
-
-  const { t } = useTranslation();
-  const { data: comments, mutate } = useSWRxPageComment(pageId);
-  const [formatedComments, setFormatedComments] = useState<ICommentHasIdList | null>(null);
-
-  const commentsFromOldest = useMemo(() => (formatedComments != null ? [...formatedComments].reverse() : null), [formatedComments]);
-  const commentsExceptReply: ICommentHasIdList | undefined = useMemo(
-    () => commentsFromOldest?.filter(comment => comment.replyTo == null), [commentsFromOldest],
-  );
-  const allReplies = {};
-
-  /**
-   * preprocess:
-   * parse, sanitize, convert markdown to html
-   */
-  const preprocessComment = useCallback(async(comment:string):Promise<string> => {
-
-    const { interceptorManager } = appContainer;
-    const growiRenderer = appContainer.getRenderer('comment');
-
-    const context: {markdown: string, parsedHTML: string} = { markdown: comment, parsedHTML: '' };
-
-    if (interceptorManager != null) {
-      await interceptorManager.process('preRenderComment', context);
-      await interceptorManager.process('prePreProcess', context);
-      context.markdown = await growiRenderer.preProcess(context.markdown);
-      await interceptorManager.process('postPreProcess', context);
-      context.parsedHTML = await growiRenderer.process(context.markdown);
-      await interceptorManager.process('prePostProcess', context);
-      context.parsedHTML = await growiRenderer.postProcess(context.parsedHTML);
-      await interceptorManager.process('postPostProcess', context);
-      await interceptorManager.process('preRenderCommentHtml', context);
-      await interceptorManager.process('postRenderCommentHtml', context);
-    }
-
-    return context.parsedHTML;
-
-  }, [appContainer]);
-
-  const highlightComment = useCallback((comment: string):string => {
-    if (highlightKeywords == null) return comment;
-
-    let highlightedComment = '';
-    highlightKeywords.forEach((highlightKeyword) => {
-      highlightedComment = comment.replaceAll(highlightKeyword, '<em class="highlighted-keyword">$&</em>');
-    });
-    return highlightedComment;
-  }, [highlightKeywords]);
-
-  useEffect(() => { mutate() }, [pageId, mutate]);
-
-  useEffect(() => {
-    const formatAndHighlightComments = async() => {
-
-      if (comments != null) {
-        const preprocessedCommentList: string[] = await Promise.all(comments.map((comment) => {
-          const highlightedComment: string = highlightComment(comment.comment);
-          return preprocessComment(highlightedComment);
-        }));
-        const preprocessedComments: ICommentHasIdList = comments.map((comment, index) => {
-          return { ...comment, comment: preprocessedCommentList[index] };
-        });
-        setFormatedComments(preprocessedComments);
-      }
-
-    };
-
-    formatAndHighlightComments();
-
-  }, [comments, highlightComment, preprocessComment]);
-
-  if (commentsFromOldest != null) {
-    commentsFromOldest.forEach((comment) => {
-      if (comment.replyTo != null) {
-        allReplies[comment.replyTo] = allReplies[comment.replyTo] == null ? [comment] : [...allReplies[comment.replyTo], comment];
-      }
-    });
-  }
-
-  const generateMarkdownBody = (comment: string): JSX.Element => {
-    const isMathJaxEnabled: boolean = (new MathJaxConfigurer(appContainer)).isEnabled;
-    return (
-      <RevisionBody
-        html={comment}
-        isMathJaxEnabled={isMathJaxEnabled}
-        renderMathJaxOnInit
-        additionalClassName="comment"
-      />
-    );
-  };
-
-  const generateBodyFromPlainText = (comment: string): JSX.Element => {
-    return <span style={{ whiteSpace: 'pre-wrap' }}>{comment}</span>;
-  };
-
-  const generateCommentInnerElement = (comment: ICommentHasId) => {
-    const revisionHref = `/${comment.page}?revision=${comment.revision}`;
-    const commentBody: string = comment.comment;
-    const formatedCommentBody = comment.isMarkdown ? generateMarkdownBody(commentBody) : generateBodyFromPlainText(commentBody);
-
-    return (
-      <div key={comment._id} className="page-comment flex-column">
-        <div className="page-comment-writer">
-          <UserPicture user={comment.creator} />
-        </div>
-        <div className="page-comment-main">
-          <div className="page-comment-creator">
-            <Username user={comment.creator} />
-          </div>
-          <div className="page-comment-body">
-            {formatedCommentBody}
-          </div>
-          <div className="page-comment-meta">
-            <a href={`/${comment.page}#${comment._id}`}>
-              <FormattedDistanceDate id={comment._id} date={comment.createdAt} />
-            </a>
-            <span className="ml-2">
-              <a id={`page-comment-revision-${comment._id}`} className="page-comment-revision" href={revisionHref}>
-                <HistoryIcon />
-              </a>
-              <UncontrolledTooltip placement="bottom" fade={false} target={`page-comment-revision-${comment._id}`}>
-                {t('page_comment.display_the_page_when_posting_this_comment')}
-              </UncontrolledTooltip>
-            </span>
-          </div>
-        </div>
-      </div>
-    );
-  };
-
-  const generateAllRepliesElement = (replyComments: ICommentHasIdList) => {
-    return (
-      replyComments.map((comment: ICommentHasId, index: number) => {
-        const lastIndex: number = replyComments.length - 1;
-        const isLastIndex: boolean = index === lastIndex;
-        const defaultReplyClasses = 'page-comment-reply ml-4 ml-sm-5 mr-3';
-        const replyClasses: string = isLastIndex ? `${defaultReplyClasses} ${COMMENT_BOTTOM_MARGIN}` : defaultReplyClasses;
-
-        return (
-          <div key={comment._id} className={replyClasses}>
-            {generateCommentInnerElement(comment)}
-          </div>
-        );
-
-      })
-    );
-  };
-
-
-  if (commentsFromOldest == null || commentsExceptReply == null) return <></>;
-
-  return (
-    <div className="page-comments-row comment-list border border-top mt-5 px-2">
-      <div className="page-comments">
-        <h2 className="text-center border-bottom my-4 pb-2"><i className="icon-fw icon-bubbles"></i>Comments</h2>
-        <div className="page-comments-list" id="page-comments-list">
-          { commentsExceptReply.map((comment, index) => {
-
-            const defaultCommentThreadClasses = 'page-comment-thread';
-            const hasReply: boolean = Object.keys(allReplies).includes(comment._id);
-            const isLastComment: boolean = index === commentsExceptReply.length - 1;
-
-            let commentThreadClasses = '';
-            commentThreadClasses = hasReply ? `${defaultCommentThreadClasses} page-comment-thread-no-replies` : defaultCommentThreadClasses;
-            commentThreadClasses = isLastComment ? `${commentThreadClasses} ${COMMENT_BOTTOM_MARGIN}` : commentThreadClasses;
-
-            return (
-              <div key={comment._id} className={commentThreadClasses}>
-                {/* display comment */}
-                {generateCommentInnerElement(comment)}
-                {/* display reply comment */}
-                {hasReply && generateAllRepliesElement(allReplies[comment._id])}
-              </div>
-            );
-
-          })}
-        </div>
-      </div>
-
-    </div>
-  );
-});
-
-
-export default PageCommentList;

+ 0 - 241
packages/app/src/components/PageComments.jsx

@@ -1,241 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import {
-  Button,
-} from 'reactstrap';
-
-import { withTranslation } from 'react-i18next';
-
-import AppContainer from '~/client/services/AppContainer';
-import CommentContainer from '~/client/services/CommentContainer';
-import PageContainer from '~/client/services/PageContainer';
-
-import { withUnstatedContainers } from './UnstatedUtils';
-
-import CommentEditor from './PageComment/CommentEditor';
-import Comment from './PageComment/Comment';
-import DeleteCommentModal from './PageComment/DeleteCommentModal';
-import ReplayComments from './PageComment/ReplayComments';
-
-
-/**
- * Load data of comments and render the list of <Comment />
- *
- * @author Yuki Takei <yuki@weseek.co.jp>
- *
- * @export
- * @class PageComments
- * @extends {React.Component}
- */
-class PageComments extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      // for deleting comment
-      commentToDelete: undefined,
-      isDeleteConfirmModalShown: false,
-      errorMessageForDeleting: undefined,
-
-      showEditorIds: new Set(),
-    };
-
-    this.growiRenderer = this.props.appContainer.getRenderer('comment');
-
-    this.init = this.init.bind(this);
-    this.confirmToDeleteComment = this.confirmToDeleteComment.bind(this);
-    this.deleteComment = this.deleteComment.bind(this);
-    this.showDeleteConfirmModal = this.showDeleteConfirmModal.bind(this);
-    this.closeDeleteConfirmModal = this.closeDeleteConfirmModal.bind(this);
-    this.replyButtonClickedHandler = this.replyButtonClickedHandler.bind(this);
-    this.editorCancelHandler = this.editorCancelHandler.bind(this);
-    this.editorCommentHandler = this.editorCommentHandler.bind(this);
-    this.resetEditor = this.resetEditor.bind(this);
-  }
-
-  componentWillMount() {
-    this.init();
-  }
-
-  init() {
-    if (!this.props.pageContainer.state.pageId) {
-      return;
-    }
-
-    this.props.commentContainer.retrieveComments();
-  }
-
-  confirmToDeleteComment(comment) {
-    this.setState({ commentToDelete: comment });
-    this.showDeleteConfirmModal();
-  }
-
-  deleteComment() {
-    const comment = this.state.commentToDelete;
-
-    this.props.commentContainer.deleteComment(comment)
-      .then(() => {
-        this.closeDeleteConfirmModal();
-      })
-      .catch((err) => {
-        this.setState({ errorMessageForDeleting: err.message });
-      });
-  }
-
-  showDeleteConfirmModal() {
-    this.setState({ isDeleteConfirmModalShown: true });
-  }
-
-  closeDeleteConfirmModal() {
-    this.setState({
-      commentToDelete: undefined,
-      isDeleteConfirmModalShown: false,
-      errorMessageForDeleting: undefined,
-    });
-  }
-
-  replyButtonClickedHandler(commentId) {
-    const ids = this.state.showEditorIds.add(commentId);
-    this.setState({ showEditorIds: ids });
-  }
-
-  editorCancelHandler(commentId) {
-    this.resetEditor(commentId);
-  }
-
-  editorCommentHandler(commentId) {
-    this.resetEditor(commentId);
-  }
-
-  resetEditor(commentId) {
-    this.setState((prevState) => {
-      prevState.showEditorIds.delete(commentId);
-      return {
-        showEditorIds: prevState.showEditorIds,
-      };
-    });
-  }
-
-  // get replies to specific comment object
-  getRepliesFor(comment, allReplies) {
-    const replyList = [];
-    allReplies.forEach((reply) => {
-      if (reply.replyTo === comment._id) {
-        replyList.push(reply);
-      }
-    });
-    return replyList;
-  }
-
-  /**
-   * render Elements of Comment Thread
-   *
-   * @param {any} comment Comment Model Obj
-   * @param {any} replies List of Reply Comment Model Obj
-   *
-   * @memberOf PageComments
-   */
-  renderThread(comment, replies) {
-    const commentId = comment._id;
-    const showEditor = this.state.showEditorIds.has(commentId);
-    const isLoggedIn = this.props.appContainer.currentUser != null;
-
-    let rootClassNames = 'page-comment-thread';
-    if (replies.length === 0) {
-      rootClassNames += ' page-comment-thread-no-replies';
-    }
-
-    return (
-      <div key={commentId} className={rootClassNames}>
-        <Comment
-          comment={comment}
-          deleteBtnClicked={this.confirmToDeleteComment}
-          growiRenderer={this.growiRenderer}
-        />
-        {replies.length !== 0 && (
-          <ReplayComments
-            replyList={replies}
-            deleteBtnClicked={this.confirmToDeleteComment}
-            growiRenderer={this.growiRenderer}
-          />
-        )}
-        { !showEditor && isLoggedIn && (
-          <div className="text-right">
-            <Button
-              outline
-              color="secondary"
-              size="sm"
-              className="btn-comment-reply"
-              onClick={() => { return this.replyButtonClickedHandler(commentId) }}
-            >
-              <i className="icon-fw icon-action-undo"></i> Reply
-            </Button>
-          </div>
-        )}
-        { showEditor && (
-          <div className="page-comment-reply-form ml-4 ml-sm-5 mr-3">
-            <CommentEditor
-              growiRenderer={this.growiRenderer}
-              replyTo={commentId}
-              onCancelButtonClicked={this.editorCancelHandler}
-              onCommentButtonClicked={this.editorCommentHandler}
-            />
-          </div>
-        )}
-      </div>
-    );
-  }
-
-  render() {
-    const topLevelComments = [];
-    const allReplies = [];
-    const comments = this.props.commentContainer.state.comments
-      .slice().reverse(); // create shallow copy and reverse
-
-    comments.forEach((comment) => {
-      if (comment.replyTo === undefined) {
-        // comment is not a reply
-        topLevelComments.push(comment);
-      }
-      else {
-        // comment is a reply
-        allReplies.push(comment);
-      }
-    });
-
-    return (
-      <div>
-        { topLevelComments.map((topLevelComment) => {
-          // get related replies
-          const replies = this.getRepliesFor(topLevelComment, allReplies);
-
-          return this.renderThread(topLevelComment, replies);
-        }) }
-
-        <DeleteCommentModal
-          isShown={this.state.isDeleteConfirmModalShown}
-          comment={this.state.commentToDelete}
-          errorMessage={this.state.errorMessageForDeleting}
-          cancel={this.closeDeleteConfirmModal}
-          confirmedToDelete={this.deleteComment}
-        />
-      </div>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const PageCommentsWrapper = withUnstatedContainers(PageComments, [AppContainer, PageContainer, CommentContainer]);
-
-PageComments.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-  commentContainer: PropTypes.instanceOf(CommentContainer).isRequired,
-};
-
-export default withTranslation()(PageCommentsWrapper);

+ 2 - 2
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -16,7 +16,7 @@ import { exportAsMarkdown } from '~/client/services/page-operation';
 import { toastSuccess } from '~/client/util/apiNotification';
 
 import PageContentFooter from '../PageContentFooter';
-import PageCommentList from '../PageCommentList';
+import PageComment from '../PageComment';
 
 import RevisionLoader from '../Page/RevisionLoader';
 import AppContainer from '../../client/services/AppContainer';
@@ -218,7 +218,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
           revisionId={page.revision}
           highlightKeywords={highlightKeywords}
         />
-        <PageCommentList appContainer={appContainer} pageId={page._id} highlightKeywords={highlightKeywords} />
+        <PageComment appContainer={appContainer} pageId={page._id} highlightKeywords={highlightKeywords} isReadOnly hideIfEmpty />
         <PageContentFooter
           createdAt={new Date(pageWithMeta.data.createdAt)}
           updatedAt={new Date(pageWithMeta.data.updatedAt)}

+ 0 - 3
packages/app/src/server/views/layout-growi/widget/comments.html

@@ -2,9 +2,6 @@
   <div class="container-lg">
 
     <div class="page-comments">
-
-      <h2 class="border-bottom pb-2 mb-3"><i class="icon-fw icon-bubbles"></i> Comments</h2>
-
       <div class="page-comments-list" id="page-comments-list"></div>
 
       {% if page and not page.isDeleted() %}