Просмотр исходного кода

Merge pull request #6459 from weseek/support/apply-nextjs-PageComment-integrate

support: Enable Editor.tsx and pull request for PageComment integrate branch
Yuki Takei 3 лет назад
Родитель
Сommit
6488ead8b0
29 измененных файлов с 735 добавлено и 588 удалено
  1. 23 0
      packages/app/src/components/PageComment.module.scss
  2. 67 51
      packages/app/src/components/PageComment.tsx
  3. 98 0
      packages/app/src/components/PageComment/Comment.module.scss
  4. 18 17
      packages/app/src/components/PageComment/Comment.tsx
  5. 11 11
      packages/app/src/components/PageComment/CommentControl.tsx
  6. 43 0
      packages/app/src/components/PageComment/CommentEditor.module.scss
  7. 32 72
      packages/app/src/components/PageComment/CommentEditor.tsx
  8. 28 14
      packages/app/src/components/PageComment/CommentEditorLazyRenderer.tsx
  9. 0 29
      packages/app/src/components/PageComment/CommentPreview.jsx
  10. 27 0
      packages/app/src/components/PageComment/CommentPreview.tsx
  11. 0 70
      packages/app/src/components/PageComment/DeleteCommentModal.jsx
  12. 10 0
      packages/app/src/components/PageComment/DeleteCommentModal.module.scss
  13. 93 0
      packages/app/src/components/PageComment/DeleteCommentModal.tsx
  14. 0 114
      packages/app/src/components/PageComment/ReplayComments.jsx
  15. 11 0
      packages/app/src/components/PageComment/ReplyComments.module.scss
  16. 101 0
      packages/app/src/components/PageComment/ReplyComments.tsx
  17. 34 0
      packages/app/src/components/PageComment/_comment-inheritance.scss
  18. 58 0
      packages/app/src/components/PageCommentSkelton.tsx
  19. 13 0
      packages/app/src/components/PageContentFooter.module.scss
  20. 18 19
      packages/app/src/components/PageContentFooter.tsx
  21. 1 0
      packages/app/src/components/PageEditor/Editor.tsx
  22. 2 7
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  23. 7 0
      packages/app/src/interfaces/editor-settings.ts
  24. 1 1
      packages/app/src/interfaces/services/renderer.ts
  25. 23 13
      packages/app/src/pages/[[...path]].page.tsx
  26. 9 0
      packages/app/src/stores/context.tsx
  27. 5 43
      packages/app/src/styles/_comment.scss
  28. 2 121
      packages/app/src/styles/_comment_growi.scss
  29. 0 6
      packages/app/src/styles/_page-content-footer.scss

+ 23 - 0
packages/app/src/components/PageComment.module.scss

@@ -0,0 +1,23 @@
+@use '~/styles/bootstrap/init' as bs;
+
+.page-comment-styles :global {
+  .page-comments {
+    h4 {
+      margin-bottom: 1em;
+    }
+  }
+
+  // reply button
+  .btn-comment-reply {
+    margin-top: 0.5em;
+    border: none;
+  }
+
+  // TODO: Refacotr Soft-coding
+  .page-comment-button-skelton {
+    width: 70.0167px;
+    height: 26.3833px;
+    margin-top: 0.5em;
+    border: none;
+  }
+}

+ 67 - 51
packages/app/src/components/PageComment.tsx

@@ -2,7 +2,7 @@ import React, {
   FC, useEffect, useState, useMemo, memo, useCallback,
   FC, useEffect, useState, useMemo, memo, useCallback,
 } from 'react';
 } from 'react';
 
 
-import { Nullable } from '@growi/core';
+import dynamic from 'next/dynamic';
 import { Button } from 'reactstrap';
 import { Button } from 'reactstrap';
 
 
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
@@ -15,19 +15,29 @@ import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
 import { useSWRxPageComment } from '../stores/comment';
 import { useSWRxPageComment } from '../stores/comment';
 
 
 import { Comment } from './PageComment/Comment';
 import { Comment } from './PageComment/Comment';
-import CommentEditor from './PageComment/CommentEditor';
-import DeleteCommentModal from './PageComment/DeleteCommentModal';
-import ReplayComments from './PageComment/ReplayComments';
+import { CommentEditorProps } from './PageComment/CommentEditor';
+import { CommentEditorLazyRenderer } from './PageComment/CommentEditorLazyRenderer';
+import { DeleteCommentModalProps } from './PageComment/DeleteCommentModal';
+import { ReplyComments } from './PageComment/ReplyComments';
+import { PageCommentSkelton } from './PageCommentSkelton';
 
 
-type Props = {
-  pageId?: Nullable<string>, // TODO: check pageId type
-  isReadOnly : boolean,
+import styles from './PageComment.module.scss';
+
+const CommentEditor = dynamic<CommentEditorProps>(() => import('./PageComment/CommentEditor').then(mod => mod.CommentEditor), { ssr: false });
+const DeleteCommentModal = dynamic<DeleteCommentModalProps>(
+  () => import('./PageComment/DeleteCommentModal').then(mod => mod.DeleteCommentModal), { ssr: false },
+);
+
+
+type PageCommentProps = {
+  pageId?: string,
+  isReadOnly: boolean,
   titleAlign?: 'center' | 'left' | 'right',
   titleAlign?: 'center' | 'left' | 'right',
-  highlightKeywords?:string[],
+  highlightKeywords?: string[],
   hideIfEmpty?: boolean,
   hideIfEmpty?: boolean,
 }
 }
 
 
-export const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
+export const PageComment: FC<PageCommentProps> = memo((props:PageCommentProps): JSX.Element => {
 
 
   const {
   const {
     pageId, highlightKeywords, isReadOnly, titleAlign, hideIfEmpty,
     pageId, highlightKeywords, isReadOnly, titleAlign, hideIfEmpty,
@@ -61,7 +71,6 @@ export const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
   }, [highlightKeywords]);
   }, [highlightKeywords]);
 
 
   useEffect(() => {
   useEffect(() => {
-
     if (comments != null) {
     if (comments != null) {
       const preprocessedCommentList: string[] = comments.map((comment) => {
       const preprocessedCommentList: string[] = comments.map((comment) => {
         const highlightedComment: string = highlightComment(comment.comment);
         const highlightedComment: string = highlightComment(comment.comment);
@@ -72,7 +81,6 @@ export const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
       });
       });
       setFormatedComments(preprocessedComments);
       setFormatedComments(preprocessedComments);
     }
     }
-
   }, [comments, highlightComment]);
   }, [comments, highlightComment]);
 
 
   if (commentsFromOldest != null) {
   if (commentsFromOldest != null) {
@@ -110,16 +118,6 @@ export const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
     }
     }
   }, [commentToBeDeleted, onDeleteCommentAfterOperation]);
   }, [commentToBeDeleted, onDeleteCommentAfterOperation]);
 
 
-  const generateAllRepliesElement = (replyComments: ICommentHasIdList) => (
-    // TODO: need page props path
-    <ReplayComments
-      replyList={replyComments}
-      deleteBtnClicked={onClickDeleteButton}
-      rendererOptions={rendererOptions}
-      isReadOnly={isReadOnly}
-    />
-  );
-
   const removeShowEditorId = useCallback((commentId: string) => {
   const removeShowEditorId = useCallback((commentId: string) => {
     setShowEditorIds((previousState) => {
     setShowEditorIds((previousState) => {
       const previousShowEditorIds = new Set(...previousState);
       const previousShowEditorIds = new Set(...previousState);
@@ -128,37 +126,52 @@ export const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
     });
     });
   }, []);
   }, []);
 
 
-
-  if (commentsFromOldest == null || commentsExceptReply == null) return <></>;
-
   if (hideIfEmpty && comments?.length === 0) {
   if (hideIfEmpty && comments?.length === 0) {
     return <></>;
     return <></>;
   }
   }
-  if (rendererOptions == null || currentPagePath == null) {
-    return <></>;
+
+  let commentTitleClasses = 'border-bottom py-3 mb-3';
+  commentTitleClasses = titleAlign != null ? `${commentTitleClasses} text-${titleAlign}` : `${commentTitleClasses} text-center`;
+
+  if (commentsFromOldest == null || commentsExceptReply == null || rendererOptions == null || currentPagePath == null || currentPage == null) {
+    if (hideIfEmpty && comments?.length === 0) {
+      return <></>;
+    }
+    return (
+      <PageCommentSkelton commentTitleClasses={commentTitleClasses}/>
+    );
   }
   }
 
 
-  const generateCommentInnerElement = (comment: ICommentHasId) => (
-    currentPage != null && (
-      <Comment
-        rendererOptions={rendererOptions}
-        deleteBtnClicked={onClickDeleteButton}
-        comment={comment}
-        onComment={mutate}
-        isReadOnly={isReadOnly}
-        currentPagePath={currentPagePath}
-        currentRevisionId={currentPage.revision._id}
-        currentRevisionCreatedAt={currentPage.revision.createdAt}
-      />
-    )
+  const generateCommentElement = (comment: ICommentHasId) => (
+    <Comment
+      comment={comment}
+      isReadOnly={isReadOnly}
+      deleteBtnClicked={onClickDeleteButton}
+      onComment={mutate}
+      rendererOptions={rendererOptions}
+      currentPagePath={currentPagePath}
+      currentRevisionId={currentPage.revision._id}
+      currentRevisionCreatedAt={currentPage.revision.createdAt}
+    />
   );
   );
 
 
-  let commentTitleClasses = 'border-bottom py-3 mb-3';
-  commentTitleClasses = titleAlign != null ? `${commentTitleClasses} text-${titleAlign}` : `${commentTitleClasses} text-center`;
+  const generateReplyCommentsElement = (replyComments: ICommentHasIdList) => (
+    <ReplyComments
+      isReadOnly={isReadOnly}
+      replyList={replyComments}
+      deleteBtnClicked={onClickDeleteButton}
+      onComment={mutate}
+      rendererOptions={rendererOptions}
+      currentPagePath={currentPagePath}
+      currentRevisionId={currentPage.revision._id}
+      currentRevisionCreatedAt={currentPage.revision.createdAt}
+    />
+  );
 
 
   return (
   return (
     <>
     <>
-      <div className="page-comments-row comment-list">
+      {/* TODO: Check the comment.html CSS */}
+      <div className={`${styles['page-comment-styles']} page-comments-row comment-list`}>
         <div className="container-lg">
         <div className="container-lg">
           <div className="page-comments">
           <div className="page-comments">
             <h2 className={commentTitleClasses}><i className="icon-fw icon-bubbles"></i>Comments</h2>
             <h2 className={commentTitleClasses}><i className="icon-fw icon-bubbles"></i>Comments</h2>
@@ -173,11 +186,8 @@ export const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
 
 
                 return (
                 return (
                   <div key={comment._id} className={commentThreadClasses}>
                   <div key={comment._id} className={commentThreadClasses}>
-                    {/* display comment */}
-                    {generateCommentInnerElement(comment)}
-                    {/* display reply comment */}
-                    {hasReply && generateAllRepliesElement(allReplies[comment._id])}
-                    {/* display reply button */}
+                    {generateCommentElement(comment)}
+                    {hasReply && generateReplyCommentsElement(allReplies[comment._id])}
                     {(!isReadOnly && !showEditorIds.has(comment._id)) && (
                     {(!isReadOnly && !showEditorIds.has(comment._id)) && (
                       <div className="text-right">
                       <div className="text-right">
                         <Button
                         <Button
@@ -193,7 +203,6 @@ export const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
                         </Button>
                         </Button>
                       </div>
                       </div>
                     )}
                     )}
-                    {/* display reply editor */}
                     {(!isReadOnly && showEditorIds.has(comment._id)) && (
                     {(!isReadOnly && showEditorIds.has(comment._id)) && (
                       <CommentEditor
                       <CommentEditor
                         rendererOptions={rendererOptions}
                         rendererOptions={rendererOptions}
@@ -212,16 +221,23 @@ export const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
 
 
               })}
               })}
             </div>
             </div>
+            {/* TODO: Check if identical-page */}
+            {(!isReadOnly) && (
+              <CommentEditorLazyRenderer
+                pageId={pageId}
+                rendererOptions={rendererOptions}
+              />
+            )}
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
-      {(!isReadOnly && commentToBeDeleted != null) && (
+      {!isReadOnly && (
         <DeleteCommentModal
         <DeleteCommentModal
           isShown={isDeleteConfirmModalShown}
           isShown={isDeleteConfirmModalShown}
           comment={commentToBeDeleted}
           comment={commentToBeDeleted}
           errorMessage={errorMessageOnDelete}
           errorMessage={errorMessageOnDelete}
-          cancel={onCancelDeleteComment}
-          confirmedToDelete={onDeleteComment}
+          cancelToDelete={onCancelDeleteComment}
+          confirmToDelete={onDeleteComment}
         />
         />
       )}
       )}
     </>
     </>

+ 98 - 0
packages/app/src/components/PageComment/Comment.module.scss

@@ -0,0 +1,98 @@
+@use '../../styles/bootstrap/init' as bs;
+@use './_comment-inheritance';
+
+.comment-styles :global {
+  .page-comment-writer {
+    @include bs.media-breakpoint-down(xs) {
+      height: 3.5em;
+    }
+  }
+
+  .page-comment {
+    position: relative;
+    padding-top: 70px;
+    margin-top: -70px;
+    pointer-events: none;
+
+    // user name
+    .page-comment-creator {
+      margin-top: -0.5em;
+      margin-bottom: 0.5em;
+      font-weight: bold;
+    }
+
+    // user icon
+    .picture {
+      @extend %picture;
+    }
+
+    // comment section
+    .page-comment-main {
+      @extend %comment-section;
+      @include bs.media-breakpoint-up(sm) {
+        margin-left: 4.5em;
+      }
+      @include bs.media-breakpoint-down(xs) {
+        &:before {
+          content: none;
+        }
+      }
+
+      pointer-events: auto;
+
+      // delete button
+      .page-comment-control {
+        position: absolute;
+        top: 0;
+        right: 0;
+        visibility: hidden;
+      }
+
+      &:hover > .page-comment-control {
+        visibility: visible;
+      }
+    }
+
+    // comment body
+    .page-comment-body {
+      margin-bottom: 0.5em;
+      word-wrap: break-word;
+    }
+
+    // older comments
+    &.page-comment-older {
+    }
+    // newer comments
+    &.page-comment-newer {
+      opacity: 0.7;
+
+      &:hover {
+        opacity: 1;
+      }
+    }
+
+    .page-comment-meta {
+      display: flex;
+      justify-content: flex-end;
+
+      font-size: 0.9em;
+      color: bs.$gray-400;
+    }
+
+    .page-comment-revision svg {
+      width: 16px;
+      height: 16px;
+    }
+  }
+
+  // TODO: Refacotr Soft-coding
+  .page-comment-comment-body-skelton {
+    position: relative;
+    height: 66px;
+    padding: 1em;
+    margin-left: 4.5em;
+    @include bs.media-breakpoint-down(xs) {
+      margin-left: 3.5em;
+    }
+  }
+}

+ 18 - 17
packages/app/src/components/PageComment/Comment.tsx

@@ -1,10 +1,9 @@
 import React, { useEffect, useState } from 'react';
 import React, { useEffect, useState } from 'react';
 
 
-
 import { UserPicture } from '@growi/ui';
 import { UserPicture } from '@growi/ui';
-import { ConsoleFormattedStream } from 'browser-bunyan';
 import { format } from 'date-fns';
 import { format } from 'date-fns';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import dynamic from 'next/dynamic';
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
 import { RendererOptions } from '~/services/renderer/renderer';
 import { RendererOptions } from '~/services/renderer/renderer';
@@ -16,8 +15,13 @@ import HistoryIcon from '../Icons/HistoryIcon';
 import RevisionRenderer from '../Page/RevisionRenderer';
 import RevisionRenderer from '../Page/RevisionRenderer';
 import Username from '../User/Username';
 import Username from '../User/Username';
 
 
-import CommentControl from './CommentControl';
-import CommentEditor from './CommentEditor';
+import { CommentControl } from './CommentControl';
+import { CommentEditorProps } from './CommentEditor';
+
+import styles from './Comment.module.scss';
+
+const CommentEditor = dynamic<CommentEditorProps>(() => import('./CommentEditor').then(mod => mod.CommentEditor), { ssr: false });
+
 
 
 type CommentProps = {
 type CommentProps = {
   comment: ICommentHasId,
   comment: ICommentHasId,
@@ -31,9 +35,12 @@ type CommentProps = {
 }
 }
 
 
 export const Comment = (props: CommentProps): JSX.Element => {
 export const Comment = (props: CommentProps): JSX.Element => {
+
   const {
   const {
-    comment, isReadOnly, deleteBtnClicked, onComment, rendererOptions, currentPagePath, currentRevisionId, currentRevisionCreatedAt,
+    comment, isReadOnly, deleteBtnClicked, onComment, rendererOptions,
+    currentPagePath, currentRevisionId, currentRevisionCreatedAt,
   } = props;
   } = props;
+
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
 
 
@@ -66,7 +73,7 @@ export const Comment = (props: CommentProps): JSX.Element => {
     return creator.username === currentUser.username;
     return creator.username === currentUser.username;
   };
   };
 
 
-  const getRootClassName = (comment) => {
+  const getRootClassName = (comment: ICommentHasId) => {
     let className = 'page-comment flex-column';
     let className = 'page-comment flex-column';
 
 
     if (comment.revision === currentRevisionId) {
     if (comment.revision === currentRevisionId) {
@@ -86,19 +93,14 @@ export const Comment = (props: CommentProps): JSX.Element => {
     return className;
     return className;
   };
   };
 
 
-  const deleteBtnClickedHandler = (comment) => {
+  const deleteBtnClickedHandler = () => {
     deleteBtnClicked(comment);
     deleteBtnClicked(comment);
   };
   };
 
 
-  const renderText = (comment) => {
+  const renderText = (comment: string) => {
     return <span style={{ whiteSpace: 'pre-wrap' }}>{comment}</span>;
     return <span style={{ whiteSpace: 'pre-wrap' }}>{comment}</span>;
   };
   };
 
 
-  // TODO: Remove when update ReplayComments.jsx
-  if (currentPagePath == null) {
-    return <></>;
-  }
-
   const renderRevisionBody = () => {
   const renderRevisionBody = () => {
     return (
     return (
       <RevisionRenderer
       <RevisionRenderer
@@ -120,14 +122,13 @@ export const Comment = (props: CommentProps): JSX.Element => {
     : null;
     : null;
 
 
   return (
   return (
-    <>
+    <div className={`${styles['comment-styles']}`}>
       {(isReEdit && !isReadOnly) ? (
       {(isReEdit && !isReadOnly) ? (
         <CommentEditor
         <CommentEditor
           rendererOptions={rendererOptions}
           rendererOptions={rendererOptions}
+          replyTo={undefined}
           currentCommentId={commentId}
           currentCommentId={commentId}
           commentBody={comment.comment}
           commentBody={comment.comment}
-          replyTo={undefined}
-          commentCreator={creator?.username}
           onCancelButtonClicked={() => setIsReEdit(false)}
           onCancelButtonClicked={() => setIsReEdit(false)}
           onCommentButtonClicked={() => {
           onCommentButtonClicked={() => {
             setIsReEdit(false);
             setIsReEdit(false);
@@ -173,6 +174,6 @@ export const Comment = (props: CommentProps): JSX.Element => {
         </div>
         </div>
       )
       )
       }
       }
-    </>
+    </div>
   );
   );
 };
 };

+ 11 - 11
packages/app/src/components/PageComment/CommentControl.jsx → packages/app/src/components/PageComment/CommentControl.tsx

@@ -1,25 +1,25 @@
 import React from 'react';
 import React from 'react';
 
 
-import PropTypes from 'prop-types';
 
 
+type CommentControlProps = {
+  onClickEditBtn: () => void,
+  onClickDeleteBtn: () => void,
+}
+
+export const CommentControl = (props: CommentControlProps): JSX.Element => {
+
+  const { onClickEditBtn, onClickDeleteBtn } = props;
 
 
-const CommentControl = (props) => {
   return (
   return (
+    // The page-comment-control class is imported from Comment.module.scss
     <div className="page-comment-control">
     <div className="page-comment-control">
-      <button type="button" className="btn btn-link p-2" onClick={props.onClickEditBtn}>
+      <button type="button" className="btn btn-link p-2" onClick={onClickEditBtn}>
         <i className="ti ti-pencil"></i>
         <i className="ti ti-pencil"></i>
       </button>
       </button>
-      <button type="button" className="btn btn-link p-2 mr-2" onClick={props.onClickDeleteBtn}>
+      <button type="button" className="btn btn-link p-2 mr-2" onClick={onClickDeleteBtn}>
         <i className="ti ti-close"></i>
         <i className="ti ti-close"></i>
       </button>
       </button>
     </div>
     </div>
   );
   );
-};
 
 
-CommentControl.propTypes = {
-
-  onClickEditBtn: PropTypes.func.isRequired,
-  onClickDeleteBtn: PropTypes.func.isRequired,
 };
 };
-
-export default CommentControl;

+ 43 - 0
packages/app/src/components/PageComment/CommentEditor.module.scss

@@ -0,0 +1,43 @@
+@use '~/styles/bootstrap/init' as bs;
+@use './_comment-inheritance';
+
+// display cheatsheet for comment form only
+.comment-editor-styles :global {
+  .comment-form {
+    position: relative;
+    margin-top: 1em;
+
+    // user icon
+    .picture {
+      @extend %picture;
+    }
+
+    // seciton
+    .comment-form-main {
+      @extend %comment-section;
+      margin-left: 4.5em;
+      @include bs.media-breakpoint-down(xs) {
+        margin-left: 3.5em;
+      }
+    }
+
+    // textarea
+    .comment-write {
+      margin-bottom: 0.5em;
+    }
+    .comment-form-preview {
+      padding-top: 0.5em;
+    }
+  }
+
+  // TODO: Refacotr Soft-coding
+  .page-comment-commenteditorlazyrenderer-body-skelton {
+    position: relative;
+    padding: 2.258rem 2rem;
+    margin-left: 4.5em;
+    line-height: 1.5;
+    @include bs.media-breakpoint-down(xs) {
+      margin-left: 3.5em;
+    }
+  }
+}

+ 32 - 72
packages/app/src/components/PageComment/CommentEditor.tsx

@@ -4,29 +4,27 @@ import React, {
 
 
 import { UserPicture } from '@growi/ui';
 import { UserPicture } from '@growi/ui';
 import {
 import {
-  Button,
-  TabContent, TabPane,
+  Button, TabContent, TabPane,
 } from 'reactstrap';
 } from 'reactstrap';
 import * as toastr from 'toastr';
 import * as toastr from 'toastr';
 
 
 import { apiPostForm } from '~/client/util/apiv1-client';
 import { apiPostForm } from '~/client/util/apiv1-client';
-import { CustomWindow } from '~/interfaces/global';
-import { IInterceptorManager } from '~/interfaces/interceptor-manager';
 import { RendererOptions } from '~/services/renderer/renderer';
 import { RendererOptions } from '~/services/renderer/renderer';
 import { useSWRxPageComment } from '~/stores/comment';
 import { useSWRxPageComment } from '~/stores/comment';
 import {
 import {
-  useCurrentPagePath, useCurrentPageId, useCurrentUser, useRevisionId, useRendererConfig,
+  useCurrentPagePath, useCurrentPageId, useCurrentUser, useRevisionId, useIsSlackConfigured,
+  useIsUploadableFile, useIsUploadableImage,
 } from '~/stores/context';
 } from '~/stores/context';
 import { useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
 import { useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
-import { useIsMobile } from '~/stores/ui';
-
 
 
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import NotAvailableForGuest from '../NotAvailableForGuest';
 import NotAvailableForGuest from '../NotAvailableForGuest';
-// import Editor from '../PageEditor/Editor';
+import Editor from '../PageEditor/Editor';
 import { SlackNotification } from '../SlackNotification';
 import { SlackNotification } from '../SlackNotification';
 
 
-import CommentPreview from './CommentPreview';
+import { CommentPreview } from './CommentPreview';
+
+import styles from './CommentEditor.module.scss';
 
 
 
 
 const navTabMapping = {
 const navTabMapping = {
@@ -42,13 +40,12 @@ const navTabMapping = {
   },
   },
 };
 };
 
 
-type PropsType = {
+export type CommentEditorProps = {
   rendererOptions: RendererOptions,
   rendererOptions: RendererOptions,
   isForNewComment?: boolean,
   isForNewComment?: boolean,
   replyTo?: string,
   replyTo?: string,
   currentCommentId?: string,
   currentCommentId?: string,
   commentBody?: string,
   commentBody?: string,
-  commentCreator?: string,
   onCancelButtonClicked?: () => void,
   onCancelButtonClicked?: () => void,
   onCommentButtonClicked?: () => void,
   onCommentButtonClicked?: () => void,
 }
 }
@@ -59,71 +56,35 @@ type EditorRef = {
   terminateUploadingState: () => void,
   terminateUploadingState: () => void,
 }
 }
 
 
-const CommentEditor = (props: PropsType): JSX.Element => {
+export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
 
 
   const {
   const {
     rendererOptions, isForNewComment, replyTo,
     rendererOptions, isForNewComment, replyTo,
-    currentCommentId, commentBody, commentCreator, onCancelButtonClicked, onCommentButtonClicked,
+    currentCommentId, commentBody, onCancelButtonClicked, onCommentButtonClicked,
   } = props;
   } = props;
+
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPageId } = useCurrentPageId();
   const { data: currentPageId } = useCurrentPageId();
   const { update: updateComment, post: postComment } = useSWRxPageComment(currentPageId);
   const { update: updateComment, post: postComment } = useSWRxPageComment(currentPageId);
   const { data: revisionId } = useRevisionId();
   const { data: revisionId } = useRevisionId();
-  const { data: isMobile } = useIsMobile();
   const { data: isSlackEnabled, mutate: mutateIsSlackEnabled } = useIsSlackEnabled();
   const { data: isSlackEnabled, mutate: mutateIsSlackEnabled } = useIsSlackEnabled();
   const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
   const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
-  const { data: config } = useRendererConfig();
-
-  // const isUploadable = config.upload.image || config.upload.file;
-  // const isUploadableFile = config.upload.file;
-  // const isSlackConfigured = config.isSlackConfigured;
+  const { data: isSlackConfigured } = useIsSlackConfigured();
+  const { data: isUploadableFile } = useIsUploadableFile();
+  const { data: isUploadableImage } = useIsUploadableImage();
 
 
   const [isReadyToUse, setIsReadyToUse] = useState(!isForNewComment);
   const [isReadyToUse, setIsReadyToUse] = useState(!isForNewComment);
   const [comment, setComment] = useState(commentBody ?? '');
   const [comment, setComment] = useState(commentBody ?? '');
-  const [html, setHtml] = useState('');
   const [activeTab, setActiveTab] = useState('comment_editor');
   const [activeTab, setActiveTab] = useState('comment_editor');
   const [error, setError] = useState();
   const [error, setError] = useState();
   const [slackChannels, setSlackChannels] = useState(slackChannelsData?.toString());
   const [slackChannels, setSlackChannels] = useState(slackChannelsData?.toString());
 
 
   const editorRef = useRef<EditorRef>(null);
   const editorRef = useRef<EditorRef>(null);
 
 
-  const renderHtml = useCallback((markdown: string) => {
-    const context = {
-      markdown,
-      parsedHTML: '',
-    };
-
-    // TODO: use ReactMarkdown
-
-    // const interceptorManager: IInterceptorManager = (window as CustomWindow).interceptorManager;
-    // interceptorManager.process('preRenderCommnetPreview', context)
-    //   .then(() => { return interceptorManager.process('prePreProcess', context) })
-    //   .then(() => {
-    //     context.markdown = rendererOptions.preProcess(context.markdown, context);
-    //   })
-    //   .then(() => { return interceptorManager.process('postPreProcess', context) })
-    //   .then(() => {
-    //     const parsedHTML = rendererOptions.process(context.markdown, context);
-    //     context.parsedHTML = parsedHTML;
-    //   })
-    //   .then(() => { return interceptorManager.process('prePostProcess', context) })
-    //   .then(() => {
-    //     context.parsedHTML = rendererOptions.postProcess(context.parsedHTML, context);
-    //   })
-    //   .then(() => { return interceptorManager.process('postPostProcess', context) })
-    //   .then(() => { return interceptorManager.process('preRenderCommentPreviewHtml', context) })
-    //   .then(() => {
-    //     setHtml(context.parsedHTML);
-    //   })
-    //   // process interceptors for post rendering
-    //   .then(() => { return interceptorManager.process('postRenderCommentPreviewHtml', context) });
-  }, [rendererOptions]);
-
   const handleSelect = useCallback((activeTab: string) => {
   const handleSelect = useCallback((activeTab: string) => {
     setActiveTab(activeTab);
     setActiveTab(activeTab);
-    renderHtml(comment);
-  }, [comment, renderHtml]);
+  }, []);
 
 
   useEffect(() => {
   useEffect(() => {
     if (slackChannels === undefined) { return }
     if (slackChannels === undefined) { return }
@@ -132,7 +93,6 @@ const CommentEditor = (props: PropsType): JSX.Element => {
 
 
   const initializeEditor = useCallback(() => {
   const initializeEditor = useCallback(() => {
     setComment('');
     setComment('');
-    setHtml('');
     setActiveTab('comment_editor');
     setActiveTab('comment_editor');
     setError(undefined);
     setError(undefined);
     // reset value
     // reset value
@@ -210,7 +170,6 @@ const CommentEditor = (props: PropsType): JSX.Element => {
   }, []);
   }, []);
 
 
   const uploadHandler = useCallback(async(file) => {
   const uploadHandler = useCallback(async(file) => {
-
     if (editorRef.current == null) { return }
     if (editorRef.current == null) { return }
 
 
     const pagePath = currentPagePath;
     const pagePath = currentPagePath;
@@ -220,6 +179,7 @@ const CommentEditor = (props: PropsType): JSX.Element => {
     formData.append('file', file);
     formData.append('file', file);
     formData.append('path', pagePath ?? '');
     formData.append('path', pagePath ?? '');
     formData.append('page_id', pageId ?? '');
     formData.append('page_id', pageId ?? '');
+
     try {
     try {
       // TODO: typescriptize res
       // TODO: typescriptize res
       const res = await apiPostForm(endpoint, formData) as any;
       const res = await apiPostForm(endpoint, formData) as any;
@@ -242,12 +202,18 @@ const CommentEditor = (props: PropsType): JSX.Element => {
   }, [apiErrorHandler, currentPageId, currentPagePath]);
   }, [apiErrorHandler, currentPageId, currentPagePath]);
 
 
   const getCommentHtml = useCallback(() => {
   const getCommentHtml = useCallback(() => {
+    if (currentPagePath == null) {
+      return <></>;
+    }
+
     return (
     return (
       <CommentPreview
       <CommentPreview
-        html={html}
+        rendererOptions={rendererOptions}
+        markdown={comment}
+        path={currentPagePath}
       />
       />
     );
     );
-  }, [html]);
+  }, [currentPagePath, comment, rendererOptions]);
 
 
   const renderBeforeReady = useCallback((): JSX.Element => {
   const renderBeforeReady = useCallback((): JSX.Element => {
     return (
     return (
@@ -266,7 +232,6 @@ const CommentEditor = (props: PropsType): JSX.Element => {
   }, []);
   }, []);
 
 
   const renderReady = () => {
   const renderReady = () => {
-
     const commentPreview = getCommentHtml();
     const commentPreview = getCommentHtml();
 
 
     const errorMessage = <span className="text-danger text-right mr-2">{error}</span>;
     const errorMessage = <span className="text-danger text-right mr-2">{error}</span>;
@@ -286,8 +251,7 @@ const CommentEditor = (props: PropsType): JSX.Element => {
       </Button>
       </Button>
     );
     );
 
 
-    // // TODO: typescriptize Editor
-    // const AnyEditor = Editor as any;
+    const isUploadable = isUploadableImage || isUploadableFile;
 
 
     return (
     return (
       <>
       <>
@@ -295,18 +259,16 @@ const CommentEditor = (props: PropsType): JSX.Element => {
           <CustomNavTab activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={handleSelect} hideBorderBottom />
           <CustomNavTab activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={handleSelect} hideBorderBottom />
           <TabContent activeTab={activeTab}>
           <TabContent activeTab={activeTab}>
             <TabPane tabId="comment_editor">
             <TabPane tabId="comment_editor">
-              {/* <AnyEditor
+              <Editor
                 ref={editorRef}
                 ref={editorRef}
                 value={comment}
                 value={comment}
-                lineNumbers={false}
-                isMobile={isMobile}
-                // isUploadable={isUploadable}
-                // isUploadableFile={isUploadableFile}
+                isUploadable={isUploadable}
+                isUploadableFile={isUploadableFile}
                 onChange={setComment}
                 onChange={setComment}
                 onUpload={uploadHandler}
                 onUpload={uploadHandler}
                 onCtrlEnter={ctrlEnterHandler}
                 onCtrlEnter={ctrlEnterHandler}
                 isComment
                 isComment
-              /> */}
+              />
               {/*
               {/*
                 Note: <OptionsSelector /> is not optimized for ComentEditor in terms of responsive design.
                 Note: <OptionsSelector /> is not optimized for ComentEditor in terms of responsive design.
                 See a review comment in https://github.com/weseek/growi/pull/3473
                 See a review comment in https://github.com/weseek/growi/pull/3473
@@ -325,7 +287,7 @@ const CommentEditor = (props: PropsType): JSX.Element => {
             <span className="flex-grow-1" />
             <span className="flex-grow-1" />
             <span className="d-none d-sm-inline">{ errorMessage && errorMessage }</span>
             <span className="d-none d-sm-inline">{ errorMessage && errorMessage }</span>
 
 
-            {/* { isSlackConfigured
+            { isSlackConfigured
               && (
               && (
                 <div className="form-inline align-self-center mr-md-2">
                 <div className="form-inline align-self-center mr-md-2">
                   <SlackNotification
                   <SlackNotification
@@ -337,7 +299,7 @@ const CommentEditor = (props: PropsType): JSX.Element => {
                   />
                   />
                 </div>
                 </div>
               )
               )
-            } */}
+            }
             <div className="d-none d-sm-block">
             <div className="d-none d-sm-block">
               <span className="mr-2">{cancelButton}</span><span>{submitButton}</span>
               <span className="mr-2">{cancelButton}</span><span>{submitButton}</span>
             </div>
             </div>
@@ -354,7 +316,7 @@ const CommentEditor = (props: PropsType): JSX.Element => {
   };
   };
 
 
   return (
   return (
-    <div className="form page-comment-form">
+    <div className={`${styles['comment-editor-styles']} form page-comment-form`}>
       <div className="comment-form">
       <div className="comment-form">
         <div className="comment-form-user">
         <div className="comment-form-user">
           <UserPicture user={currentUser} noLink noTooltip />
           <UserPicture user={currentUser} noLink noTooltip />
@@ -370,5 +332,3 @@ const CommentEditor = (props: PropsType): JSX.Element => {
   );
   );
 
 
 };
 };
-
-export default CommentEditor;

+ 28 - 14
packages/app/src/components/PageComment/CommentEditorLazyRenderer.tsx

@@ -1,33 +1,47 @@
-import React, { FC } from 'react';
+import React from 'react';
 
 
-import { useCommentPreviewOptions } from '~/stores/renderer';
+import dynamic from 'next/dynamic';
+
+import { RendererOptions } from '~/services/renderer/renderer';
 
 
 import { useSWRxPageComment } from '../../stores/comment';
 import { useSWRxPageComment } from '../../stores/comment';
+import { Skelton } from '../Skelton';
+
+import { CommentEditorProps } from './CommentEditor';
+
+import CommentEditorStyles from './CommentEditor.module.scss';
 
 
-import CommentEditor from './CommentEditor';
+const CommentEditor = dynamic<CommentEditorProps>(() => import('./CommentEditor').then(mod => mod.CommentEditor),
+  {
+    ssr: false,
+    loading: () => <div className={`${CommentEditorStyles['comment-editor-styles']} form page-comment-form`}>
+      <div className='comment-form'>
+        <div className='comment-form-user'>
+          <Skelton additionalClass='rounded-circle picture' roundedPill />
+        </div>
+        <Skelton additionalClass="page-comment-commenteditorlazyrenderer-body-skelton grw-skelton" />
+      </div>
+    </div>,
+  });
 
 
 type Props = {
 type Props = {
-  pageId: string,
+  pageId?: string,
+  rendererOptions: RendererOptions,
 }
 }
 
 
-const CommentEditorLazyRenderer:FC<Props> = (props:Props):JSX.Element => {
+export const CommentEditorLazyRenderer = (props: Props): JSX.Element => {
 
 
-  const { pageId } = props;
-  const { mutate } = useSWRxPageComment(pageId);
-  const { data: rendererOptions } = useCommentPreviewOptions();
+  const { pageId, rendererOptions } = props;
 
 
-  if (rendererOptions == null) {
-    return <></>;
-  }
+  const { mutate } = useSWRxPageComment(pageId);
 
 
   return (
   return (
     <CommentEditor
     <CommentEditor
       rendererOptions={rendererOptions}
       rendererOptions={rendererOptions}
+      isForNewComment
       replyTo={undefined}
       replyTo={undefined}
       onCommentButtonClicked={mutate}
       onCommentButtonClicked={mutate}
-      isForNewComment
     />
     />
   );
   );
-};
 
 
-export default CommentEditorLazyRenderer;
+};

+ 0 - 29
packages/app/src/components/PageComment/CommentPreview.jsx

@@ -1,29 +0,0 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
-
-import RevisionBody from '../Page/RevisionBody';
-
-/**
- * Wrapper component for Page/RevisionBody
- */
-const CommentPreview = (props) => {
-
-  return (
-    <div className="page-comment-preview-body">
-      <RevisionBody
-        html={props.html}
-        additionalClassName="comment"
-        isMathJaxEnabled
-        renderMathJaxInRealtime
-      />
-    </div>
-  );
-
-};
-
-CommentPreview.propTypes = {
-  html: PropTypes.string,
-};
-
-export default CommentPreview;

+ 27 - 0
packages/app/src/components/PageComment/CommentPreview.tsx

@@ -0,0 +1,27 @@
+import { RendererOptions } from '~/services/renderer/renderer';
+
+import RevisionRenderer from '../Page/RevisionRenderer';
+
+
+type CommentPreviewPorps = {
+  rendererOptions: RendererOptions,
+  markdown: string,
+  path: string,
+}
+
+export const CommentPreview = (props: CommentPreviewPorps): JSX.Element => {
+
+  const { rendererOptions, markdown, path } = props;
+
+  return (
+    <div className="page-comment-preview-body">
+      <RevisionRenderer
+        rendererOptions={rendererOptions}
+        markdown={markdown}
+        additionalClassName="comment"
+        pagePath={path}
+      />
+    </div>
+  );
+
+};

+ 0 - 70
packages/app/src/components/PageComment/DeleteCommentModal.jsx

@@ -1,70 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import {
-  Button, Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
-
-import { format } from 'date-fns';
-
-import { UserPicture } from '@growi/ui';
-import Username from '../User/Username';
-
-export default class DeleteCommentModal extends React.Component {
-
-  /*
-   * the threshold for omitting body
-   */
-  static get OMIT_BODY_THRES() { return 400 }
-
-  UNSAFE_componentWillMount() {
-  }
-
-  render() {
-    if (this.props.comment === undefined) {
-      return <div></div>;
-    }
-
-    const comment = this.props.comment;
-    const commentDate = format(new Date(comment.createdAt), 'yyyy/MM/dd HH:mm');
-
-    // generate body
-    let commentBody = comment.comment;
-    if (commentBody.length > DeleteCommentModal.OMIT_BODY_THRES) { // omit
-      commentBody = `${commentBody.substr(0, DeleteCommentModal.OMIT_BODY_THRES)}...`;
-    }
-    commentBody = <span style={{ whiteSpace: 'pre-wrap' }}>{commentBody}</span>;
-
-    return (
-      <Modal isOpen={this.props.isShown} toggle={this.props.cancel} className="page-comment-delete-modal">
-        <ModalHeader tag="h4" toggle={this.props.cancel} className="bg-danger text-light">
-          <span>
-            <i className="icon-fw icon-fire"></i>
-            Delete comment?
-          </span>
-        </ModalHeader>
-        <ModalBody>
-          <UserPicture user={comment.creator} size="xs" /> <strong><Username user={comment.creator}></Username></strong> wrote on {commentDate}:
-          <p className="card well comment-body mt-2 p-2">{commentBody}</p>
-        </ModalBody>
-        <ModalFooter>
-          <span className="text-danger">{this.props.errorMessage}</span>&nbsp;
-          <Button onClick={this.props.cancel}>Cancel</Button>
-          <Button color="danger" onClick={this.props.confirmedToDelete}>
-            <i className="icon icon-fire"></i>
-            Delete
-          </Button>
-        </ModalFooter>
-      </Modal>
-    );
-  }
-
-}
-
-DeleteCommentModal.propTypes = {
-  isShown: PropTypes.bool.isRequired,
-  comment: PropTypes.object,
-  errorMessage: PropTypes.string,
-  cancel: PropTypes.func.isRequired, // for cancel evnet handling
-  confirmedToDelete: PropTypes.func.isRequired, // for confirmed event handling
-};

+ 10 - 0
packages/app/src/components/PageComment/DeleteCommentModal.module.scss

@@ -0,0 +1,10 @@
+// modal
+.page-comment-delete-modal :global {
+  .modal-content .modal-body {
+    .comment-body {
+      max-height: 13em;
+      // scrollable
+      overflow-y: auto;
+    }
+  }
+}

+ 93 - 0
packages/app/src/components/PageComment/DeleteCommentModal.tsx

@@ -0,0 +1,93 @@
+import React, { useMemo } from 'react';
+
+import { UserPicture } from '@growi/ui';
+import { format } from 'date-fns';
+import {
+  Button, Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+
+import { ICommentHasId } from '../../interfaces/comment';
+import Username from '../User/Username';
+
+import styles from './DeleteCommentModal.module.scss';
+
+
+export type DeleteCommentModalProps = {
+  isShown: boolean,
+  comment: ICommentHasId | null,
+  errorMessage: string,
+  cancelToDelete: () => void,
+  confirmToDelete: () => void,
+}
+
+export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element => {
+  const {
+    isShown, comment, errorMessage, cancelToDelete, confirmToDelete,
+  } = props;
+
+  const HeaderContent = useMemo(() => {
+    if (comment == null || isShown === false) {
+      return <></>;
+    }
+    return (
+      <span>
+        <i className="icon-fw icon-fire"></i>
+        Delete comment?
+      </span>
+    );
+  }, [comment, isShown]);
+
+  const BodyContent = useMemo(() => {
+    if (comment == null || isShown === false) {
+      return <></>;
+    }
+
+    // the threshold for omitting body
+    const OMIT_BODY_THRES = 400;
+
+    const commentDate = format(new Date(comment.createdAt), 'yyyy/MM/dd HH:mm');
+
+    let commentBody = comment.comment;
+    if (commentBody.length > OMIT_BODY_THRES) { // omit
+      commentBody = `${commentBody.substr(0, OMIT_BODY_THRES)}...`;
+    }
+    const commentBodyElement = <span style={{ whiteSpace: 'pre-wrap' }}>{commentBody}</span>;
+
+    return (
+      <>
+        <UserPicture user={comment.creator} size="xs" /> <strong><Username user={comment.creator}></Username></strong> wrote on {commentDate}:
+        <p className="card well comment-body mt-2 p-2">{commentBodyElement}</p>
+      </>
+    );
+  }, [comment, isShown]);
+
+  const FooterContent = useMemo(() => {
+    if (comment == null || isShown === false) {
+      return <></>;
+    }
+    return (
+      <>
+        <span className="text-danger">{errorMessage}</span>&nbsp;
+        <Button onClick={cancelToDelete}>Cancel</Button>
+        <Button color="danger" onClick={confirmToDelete}>
+          <i className="icon icon-fire"></i>
+          Delete
+        </Button>
+      </>
+    );
+  }, [cancelToDelete, comment, confirmToDelete, errorMessage, isShown]);
+
+  return (
+    <Modal isOpen={isShown} toggle={cancelToDelete} className={`${styles['page-comment-delete-modal']}`}>
+      <ModalHeader tag="h4" toggle={cancelToDelete} className="bg-danger text-light">
+        {HeaderContent}
+      </ModalHeader>
+      <ModalBody>
+        {BodyContent}
+      </ModalBody>
+      <ModalFooter>
+        {FooterContent}
+      </ModalFooter>
+    </Modal>
+  );
+};

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

@@ -1,114 +0,0 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
-import { Collapse } from 'reactstrap';
-
-import { RendererOptions } from '~/services/renderer/renderer';
-import { useRendererConfig } from '~/stores/context';
-
-import { Comment } from './Comment';
-
-
-class ReplayComments extends React.PureComponent {
-
-  constructor() {
-    super();
-
-    this.state = {
-      isOlderRepliesShown: false,
-    };
-
-    this.toggleOlderReplies = this.toggleOlderReplies.bind(this);
-  }
-
-  toggleOlderReplies() {
-    this.setState({ isOlderRepliesShown: !this.state.isOlderRepliesShown });
-  }
-
-  renderReply(reply) {
-    return (
-      <div key={reply._id} className="page-comment-reply ml-4 ml-sm-5 mr-3">
-        <Comment
-          comment={reply}
-          deleteBtnClicked={this.props.deleteBtnClicked}
-          rendererOptions={this.props.rendererOptions}
-          isReadOnly={this.props.isReadOnly}
-        />
-      </div>
-    );
-  }
-
-  render() {
-    const { config } = this.props;
-
-    const isAllReplyShown = config.isAllReplyShown || false;
-    const replyList = this.props.replyList;
-
-    if (isAllReplyShown) {
-      return (
-        <React.Fragment>
-          {replyList.map((reply) => {
-            return this.renderReply(reply);
-          })}
-        </React.Fragment>
-      );
-    }
-
-    const areThereHiddenReplies = (replyList.length > 2);
-
-    const { isOlderRepliesShown } = this.state;
-    const toggleButtonIconName = isOlderRepliesShown ? 'icon-arrow-up' : 'icon-options-vertical';
-    const toggleButtonIcon = <i className={`icon-fw ${toggleButtonIconName}`}></i>;
-    const toggleButtonLabel = isOlderRepliesShown ? '' : 'more';
-
-    const shownReplies = replyList.slice(replyList.length - 2, replyList.length);
-    const hiddenReplies = replyList.slice(0, replyList.length - 2);
-
-    const hiddenElements = hiddenReplies.map((reply) => {
-      return this.renderReply(reply);
-    });
-
-    const shownElements = shownReplies.map((reply) => {
-      return this.renderReply(reply);
-    });
-
-    return (
-      <React.Fragment>
-        {areThereHiddenReplies && (
-          <div className="page-comments-hidden-replies">
-            <Collapse isOpen={this.state.isOlderRepliesShown}>
-              <div>{hiddenElements}</div>
-            </Collapse>
-            <div className="text-center">
-              <button
-                type="button"
-                className="btn btn-link"
-                onClick={this.toggleOlderReplies}
-              >
-                {toggleButtonIcon} {toggleButtonLabel}
-              </button>
-            </div>
-          </div>
-        )}
-        {shownElements}
-
-      </React.Fragment>
-    );
-  }
-
-}
-
-ReplayComments.propTypes = {
-  rendererOptions: PropTypes.instanceOf(RendererOptions).isRequired,
-  deleteBtnClicked: PropTypes.func.isRequired,
-  isReadOnly: PropTypes.bool.isRequired,
-  replyList: PropTypes.array,
-};
-
-const ReplayCommentsWrapperFC = (props) => {
-  const { data: config } = useRendererConfig();
-
-  return <ReplayComments config={config} {...props} />;
-};
-
-export default ReplayCommentsWrapperFC;

+ 11 - 0
packages/app/src/components/PageComment/ReplyComments.module.scss

@@ -0,0 +1,11 @@
+/*
+* reply
+*/
+.page-comment-reply :global {
+  margin-top: 1em;
+}
+
+// remove margin after hidden replies
+.page-comments-hidden-replies + .page-comment-reply :global {
+  margin-top: 0;
+}

+ 101 - 0
packages/app/src/components/PageComment/ReplyComments.tsx

@@ -0,0 +1,101 @@
+
+import React, { useState } from 'react';
+
+import { Collapse } from 'reactstrap';
+
+import { RendererOptions } from '~/services/renderer/renderer';
+
+import { ICommentHasId, ICommentHasIdList } from '../../interfaces/comment';
+import { useIsAllReplyShown } from '../../stores/context';
+
+import { Comment } from './Comment';
+
+import styles from './ReplyComments.module.scss';
+
+
+type ReplycommentsProps = {
+  isReadOnly: boolean,
+  replyList: ICommentHasIdList,
+  deleteBtnClicked: (comment: ICommentHasId) => void,
+  onComment: () => void,
+  rendererOptions: RendererOptions,
+  currentPagePath: string,
+  currentRevisionId: string,
+  currentRevisionCreatedAt: Date,
+}
+
+export const ReplyComments = (props: ReplycommentsProps): JSX.Element => {
+
+  const {
+    isReadOnly, replyList, deleteBtnClicked, onComment, rendererOptions,
+    currentPagePath, currentRevisionId, currentRevisionCreatedAt,
+  } = props;
+
+  const { data: isAllReplyShown } = useIsAllReplyShown();
+
+  const [isOlderRepliesShown, setIsOlderRepliesShown] = useState(false);
+
+  const renderReply = (reply: ICommentHasId) => {
+    return (
+      <div key={reply._id} className={`${styles['page-comment-reply']} ml-4 ml-sm-5 mr-3`}>
+        <Comment
+          comment={reply}
+          isReadOnly={isReadOnly}
+          deleteBtnClicked={deleteBtnClicked}
+          onComment={onComment}
+          rendererOptions={rendererOptions}
+          currentPagePath={currentPagePath}
+          currentRevisionId={currentRevisionId}
+          currentRevisionCreatedAt={currentRevisionCreatedAt}
+        />
+      </div>
+    );
+  };
+
+  if (isAllReplyShown) {
+    return (
+      <>
+        {replyList.map((reply) => {
+          return renderReply(reply);
+        })}
+      </>
+    );
+  }
+
+  const areThereHiddenReplies = (replyList.length > 2);
+  const toggleButtonIconName = isOlderRepliesShown ? 'icon-arrow-up' : 'icon-options-vertical';
+  const toggleButtonIcon = <i className={`icon-fw ${toggleButtonIconName}`}></i>;
+  const toggleButtonLabel = isOlderRepliesShown ? '' : 'more';
+  const shownReplies = replyList.slice(replyList.length - 2, replyList.length);
+  const hiddenReplies = replyList.slice(0, replyList.length - 2);
+
+  const hiddenElements = hiddenReplies.map((reply) => {
+    return renderReply(reply);
+  });
+
+  const shownElements = shownReplies.map((reply) => {
+    return renderReply(reply);
+  });
+
+  return (
+    <>
+      {areThereHiddenReplies && (
+        <div className={`${styles['page-comments-hidden-replies']}`}>
+          <Collapse isOpen={isOlderRepliesShown}>
+            <div>{hiddenElements}</div>
+          </Collapse>
+          <div className="text-center">
+            <button
+              type="button"
+              className="btn btn-link"
+              onClick={() => setIsOlderRepliesShown(!isOlderRepliesShown)}
+            >
+              {toggleButtonIcon} {toggleButtonLabel}
+            </button>
+          </div>
+        </div>
+      )}
+      {shownElements}
+    </>
+  );
+};

+ 34 - 0
packages/app/src/components/PageComment/_comment-inheritance.scss

@@ -0,0 +1,34 @@
+@use '../../styles/bootstrap/init' as bs;
+
+%comment-section {
+  position: relative;
+  padding: 1em;
+
+  // speech balloon
+  &:before {
+    position: absolute;
+    top: 1.5em;
+    left: -1em;
+    display: block;
+    width: 0;
+    content: '';
+    border: 1em solid transparent;
+    border-left-width: 0;
+
+    @include bs.media-breakpoint-down(xs) {
+      top: 1em;
+    }
+  }
+}
+
+%picture {
+  float: left;
+  width: 3em;
+  height: 3em;
+  margin-top: 0.8em;
+
+  @include bs.media-breakpoint-down(xs) {
+    width: 2em;
+    height: 2em;
+  }
+}

+ 58 - 0
packages/app/src/components/PageCommentSkelton.tsx

@@ -0,0 +1,58 @@
+import React from 'react';
+
+import { Skelton } from './Skelton';
+
+import styles from './PageComment.module.scss';
+import CommentStyles from './PageComment/Comment.module.scss';
+import CommentEditorStyles from './PageComment/CommentEditor.module.scss';
+
+type PageCommentSkeltonProps = {
+  commentTitleClasses?: string,
+  roundedPill?: boolean,
+}
+
+export const PageCommentSkelton = (props: PageCommentSkeltonProps): JSX.Element => {
+  const {
+    commentTitleClasses,
+  } = props;
+
+  return (
+    <>
+      {/* TODO: Check the comment.html CSS */}
+      <div className={`${styles['page-comment-styles']} 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">
+              <div className={`${CommentStyles['comment-styles']} page-comment-thread pb-5  page-comment-thread-no-replies`}>
+                <div className='page-comment flex-column'>
+                  <div className='page-commnet-writer'>
+                    <Skelton additionalClass='rounded-circle picture' roundedPill />
+                  </div>
+                  <Skelton additionalClass="page-comment-comment-body-skelton grw-skelton" />
+                </div>
+                <div className='page-comment flex-column ml-4 ml-sm-5 mr-3'>
+                  <div className='page-commnet-writer mt-3'>
+                    <Skelton additionalClass='rounded-circle picture' roundedPill />
+                  </div>
+                  <Skelton additionalClass="page-comment-comment-body-skelton grw-skelton mt-3" />
+                </div>
+                <div className="text-right">
+                  <Skelton additionalClass="page-comment-button-skelton btn btn-outline-secondary btn-sm grw-skelton" />
+                </div>
+              </div>
+            </div>
+            <div className={`${CommentEditorStyles['comment-editor-styles']} form page-comment-form`}>
+              <div className='comment-form'>
+                <div className='comment-form-user'>
+                  <Skelton additionalClass='rounded-circle picture' roundedPill />
+                </div>
+                <Skelton additionalClass="page-comment-commenteditorlazyrenderer-body-skelton grw-skelton" />
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </>
+  );
+};

+ 13 - 0
packages/app/src/components/PageContentFooter.module.scss

@@ -0,0 +1,13 @@
+@use '~/styles/bootstrap/init' as bs;
+
+.page-content-footer :global {
+  border-top: solid 1px transparent;
+  .page-meta {
+    font-size: 0.95em;
+  }
+}
+// TODO: Should Soft Coding see: https://github.com/weseek/growi/pull/6404
+.page-content-footer-skelton :global {
+  width: 300px;
+  height: 20px;
+}

+ 18 - 19
packages/app/src/components/PageContentFooter.tsx

@@ -1,29 +1,30 @@
-import React, { FC, memo } from 'react';
+import React, { memo } from 'react';
 
 
-import { Ref } from '@growi/core';
+import dynamic from 'next/dynamic';
 
 
-import { IUser } from '../interfaces/user';
+import { useSWRxCurrentPage } from '~/stores/page';
 
 
-import AuthorInfo from './Navbar/AuthorInfo';
+import { Skelton } from './Skelton';
 
 
-type Props = {
-  createdAt: Date,
-  updatedAt: Date,
-  creator: any,
-  revisionAuthor: Ref<IUser>,
-}
+import styles from './PageContentFooter.module.scss';
 
 
-const PageContentFooter:FC<Props> = memo((props:Props):JSX.Element => {
-  const {
-    createdAt, updatedAt, creator, revisionAuthor,
-  } = props;
+const AuthorInfo = dynamic(() => import('./Navbar/AuthorInfo'),
+  { ssr: false, loading: () => <Skelton additionalClass={`${styles['page-content-footer-skelton']} mb-3`} /> });
+
+export const PageContentFooter = memo((): JSX.Element => {
+
+  const { data: page } = useSWRxCurrentPage();
+
+  if (page == null) {
+    return <></>;
+  }
 
 
   return (
   return (
-    <div className="page-content-footer py-4 d-edit-none d-print-none">
+    <div className={`${styles['page-content-footer']} page-content-footer py-4 d-edit-none d-print-none}`}>
       <div className="grw-container-convertible">
       <div className="grw-container-convertible">
         <div className="page-meta">
         <div className="page-meta">
-          <AuthorInfo user={creator as IUser} date={createdAt} mode="create" locate="footer" />
-          <AuthorInfo user={revisionAuthor as IUser} date={updatedAt} mode="update" locate="footer" />
+          <AuthorInfo user={page.creator} date={page.createdAt} mode="create" locate="footer" />
+          <AuthorInfo user={page.revision.author} date={page.updatedAt} mode="update" locate="footer" />
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
@@ -31,5 +32,3 @@ const PageContentFooter:FC<Props> = memo((props:Props):JSX.Element => {
 });
 });
 
 
 PageContentFooter.displayName = 'PageContentFooter';
 PageContentFooter.displayName = 'PageContentFooter';
-
-export default PageContentFooter;

+ 1 - 0
packages/app/src/components/PageEditor/Editor.tsx

@@ -35,6 +35,7 @@ type EditorPropsType = {
   onSave?: () => Promise<void>,
   onSave?: () => Promise<void>,
   onPasteFiles?: (event: Event) => void,
   onPasteFiles?: (event: Event) => void,
   onCtrlEnter?: (event: Event) => void,
   onCtrlEnter?: (event: Event) => void,
+  isComment?: boolean,
 }
 }
 
 
 type DropzoneRef = {
 type DropzoneRef = {

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

@@ -81,7 +81,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
   const SubNavButtons = dynamic<SubNavButtonsProps>(() => import('../Navbar/SubNavButtons').then(mod => mod.SubNavButtons), { ssr: false });
   const SubNavButtons = dynamic<SubNavButtonsProps>(() => import('../Navbar/SubNavButtons').then(mod => mod.SubNavButtons), { ssr: false });
   const RevisionLoader = dynamic(() => import('../Page/RevisionLoader'), { ssr: false });
   const RevisionLoader = dynamic(() => import('../Page/RevisionLoader'), { ssr: false });
   const PageComment = dynamic(() => import('../PageComment').then(mod => mod.PageComment), { ssr: false });
   const PageComment = dynamic(() => import('../PageComment').then(mod => mod.PageComment), { ssr: false });
-  const PageContentFooter = dynamic(() => import('../PageContentFooter'), { ssr: false });
+  const PageContentFooter = dynamic(() => import('../PageContentFooter').then(mod => mod.PageContentFooter), { ssr: false });
 
 
   const scrollElementRef = useRef(null);
   const scrollElementRef = useRef(null);
 
 
@@ -217,12 +217,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
           highlightKeywords={highlightKeywords}
           highlightKeywords={highlightKeywords}
         />
         />
         <PageComment pageId={page._id} highlightKeywords={highlightKeywords} isReadOnly hideIfEmpty />
         <PageComment pageId={page._id} highlightKeywords={highlightKeywords} isReadOnly hideIfEmpty />
-        <PageContentFooter
-          createdAt={new Date(pageWithMeta.data.createdAt)}
-          updatedAt={new Date(pageWithMeta.data.updatedAt)}
-          creator={pageWithMeta.data.creator}
-          revisionAuthor={pageWithMeta.data.lastUpdateUser}
-        />
+        <PageContentFooter />
       </div>
       </div>
     </div>
     </div>
   );
   );

+ 7 - 0
packages/app/src/interfaces/editor-settings.ts

@@ -29,3 +29,10 @@ export interface IEditorSettings {
   autoFormatMarkdownTable: boolean,
   autoFormatMarkdownTable: boolean,
   textlintSettings: undefined | ITextlintSettings;
   textlintSettings: undefined | ITextlintSettings;
 }
 }
+
+export type EditorConfig = {
+  upload: {
+    isUploadableFile: boolean,
+    isUploadableImage: boolean,
+  }
+}

+ 1 - 1
packages/app/src/interfaces/services/renderer.ts

@@ -13,7 +13,7 @@ export type RendererConfig = {
   isEnabledLinebreaksInComments: boolean,
   isEnabledLinebreaksInComments: boolean,
   adminPreferredIndentSize: number,
   adminPreferredIndentSize: number,
   isIndentSizeForced: boolean,
   isIndentSizeForced: boolean,
-  highlightJsStyleBorder: boolean
+  highlightJsStyleBorder: boolean,
 
 
   plantumlUri: string | null,
   plantumlUri: string | null,
   blockdiagUri: string | null,
   blockdiagUri: string | null,

+ 23 - 13
packages/app/src/pages/[[...path]].page.tsx

@@ -20,12 +20,13 @@ import superjson from 'superjson';
 import { PageAlerts } from '~/components/PageAlert/PageAlerts';
 import { PageAlerts } from '~/components/PageAlert/PageAlerts';
 import { PageComment } from '~/components/PageComment';
 import { PageComment } from '~/components/PageComment';
 // import { useTranslation } from '~/i18n';
 // import { useTranslation } from '~/i18n';
-// import CommentEditorLazyRenderer from '~/components/PageComment/CommentEditorLazyRenderer';
+import { PageContentFooter } from '~/components/PageContentFooter';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 // import { renderScriptTagByName, renderHighlightJsStyleTag } from '~/service/cdn-resources-loader';
 // import { renderScriptTagByName, renderHighlightJsStyleTag } from '~/service/cdn-resources-loader';
 // import { useIndentSize } from '~/stores/editor';
 // import { useIndentSize } from '~/stores/editor';
 // import { useRendererSettings } from '~/stores/renderer';
 // import { useRendererSettings } from '~/stores/renderer';
 // import { EditorMode, useEditorMode, useIsMobile } from '~/stores/ui';
 // import { EditorMode, useEditorMode, useIsMobile } from '~/stores/ui';
+import { EditorConfig } from '~/interfaces/editor-settings';
 import { CustomWindow } from '~/interfaces/global';
 import { CustomWindow } from '~/interfaces/global';
 import { RendererConfig } from '~/interfaces/services/renderer';
 import { RendererConfig } from '~/interfaces/services/renderer';
 import { ISidebarConfig } from '~/interfaces/sidebar-config';
 import { ISidebarConfig } from '~/interfaces/sidebar-config';
@@ -40,6 +41,7 @@ import {
 } from '~/stores/ui';
 } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+
 // import { isUserPage, isTrashPage, isSharedPage } from '~/utils/path-utils';
 // import { isUserPage, isTrashPage, isSharedPage } from '~/utils/path-utils';
 
 
 // import GrowiSubNavigation from '../client/js/components/Navbar/GrowiSubNavigation';
 // import GrowiSubNavigation from '../client/js/components/Navbar/GrowiSubNavigation';
@@ -60,6 +62,7 @@ import {
   useIsAclEnabled, useIsUserPage, useIsNotCreatable,
   useIsAclEnabled, useIsUserPage, useIsNotCreatable,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
   useIsSlackConfigured, useIsBlinkedHeaderAtBoot, useRendererConfig, useEditingMarkdown,
   useIsSlackConfigured, useIsBlinkedHeaderAtBoot, useRendererConfig, useEditingMarkdown,
+  useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage,
 } from '../stores/context';
 } from '../stores/context';
 
 
 import {
 import {
@@ -143,8 +146,9 @@ type Props = CommonProps & {
   // mathJax: string,
   // mathJax: string,
   // noCdn: string,
   // noCdn: string,
   // highlightJsStyle: string,
   // highlightJsStyle: string,
-  // isAllReplyShown: boolean,
-  // editorConfig: any,
+  isAllReplyShown: boolean,
+  // isContainerFluid: boolean,
+  editorConfig: EditorConfig,
   isEnabledStaleNotification: boolean,
   isEnabledStaleNotification: boolean,
   // isEnabledLinebreaks: boolean,
   // isEnabledLinebreaks: boolean,
   // isEnabledLinebreaksInComments: boolean,
   // isEnabledLinebreaksInComments: boolean,
@@ -177,7 +181,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   }
   }
 
 
   // commons
   // commons
-  // useEditorConfig(props.editorConfig);
+  useEditorConfig(props.editorConfig);
   useCsrfToken(props.csrfToken);
   useCsrfToken(props.csrfToken);
 
 
   // UserUISettings
   // UserUISettings
@@ -221,6 +225,10 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   useRendererConfig(props.rendererConfig);
   useRendererConfig(props.rendererConfig);
   // useRendererSettings(props.rendererSettingsStr != null ? JSON.parse(props.rendererSettingsStr) : undefined);
   // useRendererSettings(props.rendererSettingsStr != null ? JSON.parse(props.rendererSettingsStr) : undefined);
   // useGrowiRendererConfig(props.growiRendererConfigStr != null ? JSON.parse(props.growiRendererConfigStr) : undefined);
   // useGrowiRendererConfig(props.growiRendererConfigStr != null ? JSON.parse(props.growiRendererConfigStr) : undefined);
+  useIsAllReplyShown(props.isAllReplyShown);
+
+  useIsUploadableFile(props.editorConfig.upload.isUploadableFile);
+  useIsUploadableImage(props.editorConfig.upload.isUploadableImage);
 
 
   // const { data: editorMode } = useEditorMode();
   // const { data: editorMode } = useEditorMode();
 
 
@@ -322,8 +330,9 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
           </div>
           </div>
         </div>
         </div>
         <footer>
         <footer>
-          <PageComment pageId={useCurrentPageId().data} isReadOnly={false} titleAlign="left" />
-          {/* <CommentEditorLazyRenderer pageId={useCurrentPageId().data} /> */}
+          {/* <PageComments /> */}
+          <PageComment pageId={pageId} isReadOnly={false} titleAlign="left" />
+          <PageContentFooter />
         </footer>
         </footer>
 
 
         <UnsavedAlertDialog />
         <UnsavedAlertDialog />
@@ -482,17 +491,18 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   // props.mathJax = configManager.getConfig('crowi', 'app:mathJax');
   // props.mathJax = configManager.getConfig('crowi', 'app:mathJax');
   // props.noCdn = configManager.getConfig('crowi', 'app:noCdn');
   // props.noCdn = configManager.getConfig('crowi', 'app:noCdn');
   // props.highlightJsStyle = configManager.getConfig('crowi', 'customize:highlightJsStyle');
   // props.highlightJsStyle = configManager.getConfig('crowi', 'customize:highlightJsStyle');
-  // props.isAllReplyShown = configManager.getConfig('crowi', 'customize:isAllReplyShown');
+  props.isAllReplyShown = configManager.getConfig('crowi', 'customize:isAllReplyShown');
+  // props.isContainerFluid = configManager.getConfig('crowi', 'customize:isContainerFluid');
   props.isEnabledStaleNotification = configManager.getConfig('crowi', 'customize:isEnabledStaleNotification');
   props.isEnabledStaleNotification = configManager.getConfig('crowi', 'customize:isEnabledStaleNotification');
   // props.isEnabledLinebreaks = configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks');
   // props.isEnabledLinebreaks = configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks');
   // props.isEnabledLinebreaksInComments = configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments');
   // props.isEnabledLinebreaksInComments = configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments');
   props.disableLinkSharing = configManager.getConfig('crowi', 'security:disableLinkSharing');
   props.disableLinkSharing = configManager.getConfig('crowi', 'security:disableLinkSharing');
-  // props.editorConfig = {
-  //   upload: {
-  //     image: crowi.fileUploadService.getIsUploadable(),
-  //     file: crowi.fileUploadService.getFileUploadEnabled(),
-  //   },
-  // };
+  props.editorConfig = {
+    upload: {
+      isUploadableFile: crowi.fileUploadService.getFileUploadEnabled(),
+      isUploadableImage: crowi.fileUploadService.getIsUploadable(),
+    },
+  };
   // props.adminPreferredIndentSize = configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize');
   // props.adminPreferredIndentSize = configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize');
   // props.isIndentSizeForced = configManager.getConfig('markdown', 'markdown:isIndentSizeForced');
   // props.isIndentSizeForced = configManager.getConfig('markdown', 'markdown:isIndentSizeForced');
 
 

+ 9 - 0
packages/app/src/stores/context.tsx

@@ -4,6 +4,7 @@ import useSWRImmutable from 'swr/immutable';
 
 
 
 
 import { SupportedActionType } from '~/interfaces/activity';
 import { SupportedActionType } from '~/interfaces/activity';
+import { EditorConfig } from '~/interfaces/editor-settings';
 // import { CustomWindow } from '~/interfaces/global';
 // import { CustomWindow } from '~/interfaces/global';
 import { RendererConfig } from '~/interfaces/services/renderer';
 import { RendererConfig } from '~/interfaces/services/renderer';
 import { GrowiThemes } from '~/interfaces/theme';
 import { GrowiThemes } from '~/interfaces/theme';
@@ -226,10 +227,18 @@ export const useIsLatestRevision = (initialData?: boolean): SWRResponse<boolean,
   return useStaticSWR('isLatestRevision', initialData);
   return useStaticSWR('isLatestRevision', initialData);
 };
 };
 
 
+export const useEditorConfig = (initialData?: EditorConfig): SWRResponse<EditorConfig, Error> => {
+  return useStaticSWR<EditorConfig, Error>('editorConfig', initialData);
+};
+
 export const useRendererConfig = (initialData?: RendererConfig): SWRResponse<RendererConfig, any> => {
 export const useRendererConfig = (initialData?: RendererConfig): SWRResponse<RendererConfig, any> => {
   return useStaticSWR('growiRendererConfig', initialData);
   return useStaticSWR('growiRendererConfig', initialData);
 };
 };
 
 
+export const useIsAllReplyShown = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useStaticSWR('isAllReplyShown', initialData);
+};
+
 export const useIsBlinkedHeaderAtBoot = (initialData?: boolean): SWRResponse<boolean, Error> => {
 export const useIsBlinkedHeaderAtBoot = (initialData?: boolean): SWRResponse<boolean, Error> => {
   return useStaticSWR('isBlinkedAtBoot', initialData);
   return useStaticSWR('isBlinkedAtBoot', initialData);
 };
 };

+ 5 - 43
packages/app/src/styles/_comment.scss

@@ -9,57 +9,19 @@
   }
   }
 }
 }
 
 
+
 .page-comments {
 .page-comments {
+  // TODO: Never use .page-comments-list-toggle-older class.
   .page-comments-list-toggle-older {
   .page-comments-list-toggle-older {
     display: inline-block;
     display: inline-block;
     font-size: 0.9em;
     font-size: 0.9em;
   }
   }
+  // TODO: "pointer-events: none;" moved to "Comment.module.scss" now.
+  // .page-comment was defined in _comment.scss and _comment_growi.scss
+  // Required if .page-comment is not under .growi but under .page-comments, or under .growi but not under .page-comments
   .page-comment {
   .page-comment {
     padding-top: 50px;
     padding-top: 50px;
     margin-top: -50px;
     margin-top: -50px;
     pointer-events: none;
     pointer-events: none;
   }
   }
-
-  .page-comment {
-    // older comments
-    &.page-comment-older {
-    }
-    // newer comments
-    &.page-comment-newer {
-      opacity: 0.7;
-
-      &:hover {
-        opacity: 1;
-      }
-    }
-
-    .page-comment-meta {
-      display: flex;
-      justify-content: flex-end;
-
-      font-size: 0.9em;
-      color: $gray-400;
-    }
-
-    .page-comment-revision svg {
-      width: 16px;
-      height: 16px;
-    }
-  }
-
-  .page-comment-main {
-    pointer-events: auto;
-
-    // delete button
-    .page-comment-control {
-      position: absolute;
-      top: 0;
-      right: 0;
-      visibility: hidden;
-    }
-
-    &:hover > .page-comment-control {
-      visibility: visible;
-    }
-  }
 }
 }

+ 2 - 121
packages/app/src/styles/_comment_growi.scss

@@ -1,129 +1,13 @@
 .growi {
 .growi {
-  %comment-section {
-    position: relative;
-    padding: 1em;
-
-    // speech balloon
-    &:before {
-      position: absolute;
-      top: 1.5em;
-      left: -1em;
-      display: block;
-      width: 0;
-      content: '';
-      border: 1em solid transparent;
-      border-left-width: 0;
-
-      @include media-breakpoint-down(xs) {
-        top: 1em;
-      }
-    }
-  }
-
-  %picture {
-    float: left;
-    width: 3em;
-    height: 3em;
-    margin-top: 0.8em;
-
-    @include media-breakpoint-down(xs) {
-      width: 2em;
-      height: 2em;
-    }
-  }
-
-  .page-comments {
-    h4 {
-      margin-bottom: 1em;
-    }
-  }
-
-  .page-comment-writer {
-    @include media-breakpoint-down(xs) {
-      height: 3.5em;
-    }
-  }
-
-  .page-comment {
-    position: relative;
-    padding-top: 70px;
-    margin-top: -70px;
-
-    // ユーザー名
-    .page-comment-creator {
-      margin-top: -0.5em;
-      margin-bottom: 0.5em;
-      font-weight: bold;
-    }
-
-    // ユーザーアイコン
-    .picture {
-      @extend %picture;
-    }
-
-    // コメントセクション
-    .page-comment-main {
-      @extend %comment-section;
-      @include media-breakpoint-up(sm) {
-        margin-left: 4.5em;
-      }
-      @include media-breakpoint-down(xs) {
-        &:before {
-          content: none;
-        }
-      }
-    }
-
-    // コメント本文
-    .page-comment-body {
-      margin-bottom: 0.5em;
-      word-wrap: break-word;
-    }
-  }
-
-  /*
-   * reply
-   */
-  .page-comment-reply {
-    margin-top: 1em;
-  }
-  // remove margin after hidden replies
-  .page-comments-hidden-replies + .page-comment-reply {
-    margin-top: 0;
-  }
-  // reply button
-  .btn.btn-comment-reply {
-    margin-top: 0.5em;
-    border: none;
-  }
-
   // display cheatsheet for comment form only
   // display cheatsheet for comment form only
   .comment-form {
   .comment-form {
+    // TODO: Never use .editor-cheatsheet class.
     .editor-cheatsheet {
     .editor-cheatsheet {
       display: none;
       display: none;
     }
     }
 
 
-    position: relative;
-    margin-top: 1em;
-
-    // user icon
-    .picture {
-      @extend %picture;
-    }
-
-    // seciton
-    .comment-form-main {
-      @extend %comment-section;
-      margin-left: 4.5em;
-      @include media-breakpoint-down(xs) {
-        margin-left: 3.5em;
-      }
-    }
-
     // textarea
     // textarea
-    .comment-write {
-      margin-bottom: 0.5em;
-    }
+    // TODO: Never use .comment-form-comment class.
     .comment-form-comment {
     .comment-form-comment {
       height: 80px;
       height: 80px;
       &:focus,
       &:focus,
@@ -132,8 +16,5 @@
         transition: height 0.2s ease-out;
         transition: height 0.2s ease-out;
       }
       }
     }
     }
-    .comment-form-preview {
-      padding-top: 0.5em;
-    }
   }
   }
 }
 }

+ 0 - 6
packages/app/src/styles/_page-content-footer.scss

@@ -1,6 +0,0 @@
-.page-content-footer {
-  border-top: solid 1px transparent;
-  .page-meta {
-    font-size: 0.95em;
-  }
-}