Răsfoiți Sursa

Merge pull request #1151 from weseek/imprv/page-comments

Imprv/page comments
Yuki Takei 6 ani în urmă
părinte
comite
88cb9af16b

+ 1 - 0
CHANGES.md

@@ -2,6 +2,7 @@
 
 ## 3.5.6-RC
 
+* Improvement: Show commented date with date distance format
 * Fix: Saving new page is failed when empty string tag is set
 * Fix: Link of Create template page button in New Page Modal is broken
 * Fix: Global Notification dows not work when creating/moving/deleting/like/comment

+ 1 - 1
src/client/js/components/BookmarkButton.jsx

@@ -56,7 +56,7 @@ export default class BookmarkButton extends React.Component {
   }
 
   isUserLoggedIn() {
-    return this.props.crowi.me !== '';
+    return this.props.crowi.me != null;
   }
 
   render() {

+ 1 - 1
src/client/js/components/LikeButton.jsx

@@ -37,7 +37,7 @@ class LikeButton extends React.Component {
   }
 
   isUserLoggedIn() {
-    return this.props.appContainer.me !== '';
+    return this.props.appContainer.me != null;
   }
 
   render() {

+ 1 - 1
src/client/js/components/PageAttachment.jsx

@@ -89,7 +89,7 @@ class PageAttachment extends React.Component {
   }
 
   isUserLoggedIn() {
-    return this.props.appContainer.me !== '';
+    return this.props.appContainer.me != null;
   }
 
   render() {

+ 57 - 57
src/client/js/components/PageComment/Comment.jsx

@@ -4,8 +4,10 @@ import PropTypes from 'prop-types';
 import { distanceInWordsStrict } from 'date-fns';
 import dateFnsFormat from 'date-fns/format';
 
+import Button from 'react-bootstrap/es/Button';
 import Tooltip from 'react-bootstrap/es/Tooltip';
 import OverlayTrigger from 'react-bootstrap/es/OverlayTrigger';
+import Collapse from 'react-bootstrap/es/Collapse';
 
 import AppContainer from '../../services/AppContainer';
 import PageContainer from '../../services/PageContainer';
@@ -30,7 +32,7 @@ class Comment extends React.Component {
 
     this.state = {
       html: '',
-      isLayoutTypeGrowi: false,
+      isOlderRepliesShown: false,
     };
 
     this.isCurrentUserIsAuthor = this.isCurrentUserEqualsToAuthor.bind(this);
@@ -44,12 +46,6 @@ class Comment extends React.Component {
 
   componentWillMount() {
     this.renderHtml(this.props.comment.comment);
-    this.init();
-  }
-
-  init() {
-    const layoutType = this.props.appContainer.getConfig().layoutType;
-    this.setState({ isLayoutTypeGrowi: layoutType === 'crowi-plus' || layoutType === 'growi' });
   }
 
   componentWillReceiveProps(nextProps) {
@@ -155,65 +151,67 @@ class Comment extends React.Component {
 
   }
 
+  renderReply(reply) {
+    return (
+      <div key={reply._id} className="page-comment-reply">
+        <CommentWrapper
+          comment={reply}
+          deleteBtnClicked={this.props.deleteBtnClicked}
+          growiRenderer={this.props.growiRenderer}
+        />
+      </div>
+    );
+  }
+
   renderReplies() {
-    const isLayoutTypeGrowi = this.state.isLayoutTypeGrowi;
+    const layoutType = this.props.appContainer.getConfig().layoutType;
+    const isBaloonStyle = layoutType.match(/crowi-plus|growi|kibela/);
+
     let replyList = this.props.replyList;
-    if (!isLayoutTypeGrowi) {
+    if (!isBaloonStyle) {
       replyList = replyList.slice().reverse();
     }
 
     const areThereHiddenReplies = replyList.length > 2;
 
-    const iconForOlder = <i className="icon-options-vertical"></i>;
-    const toggleOlder = areThereHiddenReplies
-      ? (
-        <a className="page-comments-list-toggle-older text-center" data-toggle="collapse" href="#page-comments-list-older">
-          {iconForOlder} Read More
-        </a>
-      )
-      : <div></div>;
+    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 toggleButton = (
+      <Button
+        bsStyle="link"
+        className="page-comments-list-toggle-older"
+        onClick={() => { this.setState({ isOlderRepliesShown: !isOlderRepliesShown }) }}
+      >
+        {toggleButtonIcon} {toggleButtonLabel}
+      </Button>
+    );
 
     const shownReplies = replyList.slice(replyList.length - 2, replyList.length);
     const hiddenReplies = replyList.slice(0, replyList.length - 2);
 
-    const toggleElements = hiddenReplies.map((reply) => {
-      return (
-        <div key={reply._id} className="col-xs-offset-1 col-xs-11 col-sm-offset-1 col-sm-11 col-md-offset-1 col-md-11 col-lg-offset-1 col-lg-11">
-          <CommentWrapper
-            comment={reply}
-            deleteBtnClicked={this.props.deleteBtnClicked}
-            growiRenderer={this.props.growiRenderer}
-            replyList={[]}
-          />
-        </div>
-      );
+    const hiddenElements = hiddenReplies.map((reply) => {
+      return this.renderReply(reply);
     });
 
-    const toggleBlock = (
-      <div className="page-comments-list-older collapse out" id="page-comments-list-older">
-        {toggleElements}
-      </div>
-    );
-
-    const shownBlock = shownReplies.map((reply) => {
-      return (
-        <div key={reply._id} className="col-xs-offset-1 col-xs-11 col-sm-offset-1 col-sm-11 col-md-offset-1 col-md-11 col-lg-offset-1 col-lg-11">
-          <CommentWrapper
-            comment={reply}
-            deleteBtnClicked={this.props.deleteBtnClicked}
-            growiRenderer={this.props.growiRenderer}
-            replyList={[]}
-          />
-        </div>
-      );
+    const shownElements = shownReplies.map((reply) => {
+      return this.renderReply(reply);
     });
 
     return (
-      <div>
-        {toggleBlock}
-        {toggleOlder}
-        {shownBlock}
-      </div>
+      <React.Fragment>
+        { areThereHiddenReplies && (
+          <div className="page-comments-hidden-replies">
+            <Collapse in={this.state.isOlderRepliesShown}>
+              <div>{hiddenElements}</div>
+            </Collapse>
+            <div className="text-center">{toggleButton}</div>
+          </div>
+        ) }
+
+        {shownElements}
+      </React.Fragment>
     );
   }
 
@@ -236,7 +234,8 @@ class Comment extends React.Component {
     );
 
     return (
-      <div>
+      <React.Fragment>
+
         <div className={rootClassName}>
           <UserPicture user={creator} />
           <div className="page-comment-main">
@@ -257,12 +256,10 @@ class Comment extends React.Component {
             </div>
           </div>
         </div>
-        <div className="container-fluid">
-          <div className="row">
-            {this.renderReplies()}
-          </div>
-        </div>
-      </div>
+
+        {this.renderReplies()}
+
+      </React.Fragment>
     );
   }
 
@@ -284,5 +281,8 @@ Comment.propTypes = {
   deleteBtnClicked: PropTypes.func.isRequired,
   replyList: PropTypes.array,
 };
+Comment.defaultProps = {
+  replyList: [],
+};
 
 export default CommentWrapper;

+ 74 - 95
src/client/js/components/PageComment/CommentEditor.jsx

@@ -36,7 +36,6 @@ class CommentEditor extends React.Component {
     const isUploadableFile = config.upload.file;
 
     this.state = {
-      isLayoutTypeGrowi: false,
       comment: '',
       isMarkdown: true,
       html: '',
@@ -60,15 +59,6 @@ class CommentEditor extends React.Component {
     this.toggleEditor = this.toggleEditor.bind(this);
   }
 
-  componentWillMount() {
-    this.init();
-  }
-
-  init() {
-    const layoutType = this.props.appContainer.getConfig().layoutType;
-    this.setState({ isLayoutTypeGrowi: layoutType === 'crowi-plus' || layoutType === 'growi' });
-  }
-
   updateState(value) {
     this.setState({ comment: value });
   }
@@ -214,7 +204,8 @@ class CommentEditor extends React.Component {
     const commentPreview = this.state.isMarkdown ? this.getCommentHtml() : null;
     const emojiStrategy = appContainer.getEmojiStrategy();
 
-    const isLayoutTypeGrowi = this.state.isLayoutTypeGrowi;
+    const layoutType = this.props.appContainer.getConfig().layoutType;
+    const isBaloonStyle = layoutType.match(/crowi-plus|growi|kibela/);
 
     const errorMessage = <span className="text-danger text-right mr-2">{this.state.errorMessage}</span>;
     const submitButton = (
@@ -229,98 +220,86 @@ class CommentEditor extends React.Component {
 
     return (
       <div className="form page-comment-form">
-
-        { username
-          && (
-          <div className="comment-form">
-            { isLayoutTypeGrowi
-              && (
-              <div className="comment-form-user">
-                <UserPicture user={user} />
-              </div>
-              )
-            }
-            <div className="comment-form-main">
-              <div className="comment-write">
-                <Tabs activeKey={this.state.key} id="comment-form-tabs" onSelect={this.handleSelect} animation={false}>
-                  <Tab eventKey={1} title="Write">
-                    <Editor
-                      ref={(c) => { this.editor = c }}
-                      value={this.state.comment}
-                      isGfmMode={this.state.isMarkdown}
-                      lineNumbers={false}
-                      isMobile={appContainer.isMobile}
-                      isUploadable={this.state.isUploadable && this.state.isLayoutTypeGrowi} // enable only when GROWI layout
-                      isUploadableFile={this.state.isUploadableFile}
-                      emojiStrategy={emojiStrategy}
-                      onChange={this.updateState}
-                      onUpload={this.uploadHandler}
-                      onCtrlEnter={this.postHandler}
-                    />
+        <div className="comment-form">
+          { isBaloonStyle && (
+            <div className="comment-form-user">
+              <UserPicture user={user} />
+            </div>
+          ) }
+          <div className="comment-form-main">
+            <div className="comment-write">
+              <Tabs activeKey={this.state.key} id="comment-form-tabs" onSelect={this.handleSelect} animation={false}>
+                <Tab eventKey={1} title="Write">
+                  <Editor
+                    ref={(c) => { this.editor = c }}
+                    value={this.state.comment}
+                    isGfmMode={this.state.isMarkdown}
+                    lineNumbers={false}
+                    isMobile={appContainer.isMobile}
+                    isUploadable={this.state.isUploadable && this.state.isLayoutTypeGrowi} // enable only when GROWI layout
+                    isUploadableFile={this.state.isUploadableFile}
+                    emojiStrategy={emojiStrategy}
+                    onChange={this.updateState}
+                    onUpload={this.uploadHandler}
+                    onCtrlEnter={this.postHandler}
+                  />
+                </Tab>
+                { this.state.isMarkdown && (
+                  <Tab eventKey={2} title="Preview">
+                    <div className="comment-form-preview">
+                      {commentPreview}
+                    </div>
                   </Tab>
-                  { this.state.isMarkdown
-                    && (
-                    <Tab eventKey={2} title="Preview">
-                      <div className="comment-form-preview">
-                        {commentPreview}
-                      </div>
-                    </Tab>
-                    )
-                  }
-                </Tabs>
-              </div>
-              <div className="comment-submit">
-                <div className="d-flex">
-                  <label style={{ flex: 1 }}>
-                    { isLayoutTypeGrowi && this.state.key === 1
-                      && (
-                      <span>
-                        <input
-                          type="checkbox"
-                          id="comment-form-is-markdown"
-                          name="isMarkdown"
-                          checked={this.state.isMarkdown}
-                          value="1"
-                          onChange={this.updateStateCheckbox}
-                        />
-                        <span className="ml-2">Markdown</span>
-                      </span>
-                      )
-                  }
-                  </label>
-                  <span className="hidden-xs">{ this.state.errorMessage && errorMessage }</span>
-                  { this.state.hasSlackConfig
-                    && (
-                    <div className="form-inline align-self-center mr-md-2">
-                      <SlackNotification
-                        isSlackEnabled={commentContainer.state.isSlackEnabled}
-                        slackChannels={commentContainer.state.slackChannels}
-                        onEnabledFlagChange={this.onSlackEnabledFlagChange}
-                        onChannelChange={this.onSlackChannelsChange}
+                ) }
+              </Tabs>
+            </div>
+            <div className="comment-submit">
+              <div className="d-flex">
+                <label style={{ flex: 1 }}>
+                  { isBaloonStyle && this.state.key === 1 && (
+                    <span>
+                      <input
+                        type="checkbox"
+                        id="comment-form-is-markdown"
+                        name="isMarkdown"
+                        checked={this.state.isMarkdown}
+                        value="1"
+                        onChange={this.updateStateCheckbox}
                       />
-                    </div>
-                    )
-                  }
-                  <div>
-                    <Button bsStyle="danger" className="fcbtn btn btn-xs btn-danger btn-outline btn-rounded" onClick={this.toggleEditor}>
-                      Cancel
-                    </Button>
+                      <span className="ml-2">Markdown</span>
+                    </span>
+                  ) }
+                </label>
+                <span className="hidden-xs">{ this.state.errorMessage && errorMessage }</span>
+                { this.state.hasSlackConfig
+                  && (
+                  <div className="form-inline align-self-center mr-md-2">
+                    <SlackNotification
+                      isSlackEnabled={commentContainer.state.isSlackEnabled}
+                      slackChannels={commentContainer.state.slackChannels}
+                      onEnabledFlagChange={this.onSlackEnabledFlagChange}
+                      onChannelChange={this.onSlackChannelsChange}
+                    />
                   </div>
-                  &nbsp;&nbsp;&nbsp;&nbsp;
-                  <div className="hidden-xs">{submitButton}</div>
+                  )
+                }
+                <div>
+                  <Button bsStyle="danger" className="fcbtn btn btn-xs btn-danger btn-outline btn-rounded" onClick={this.toggleEditor}>
+                    Cancel
+                  </Button>
                 </div>
-                <div className="visible-xs mt-2">
-                  <div className="d-flex justify-content-end">
-                    { this.state.errorMessage && errorMessage }
-                    <div>{submitButton}</div>
-                  </div>
+                &nbsp;&nbsp;&nbsp;&nbsp;
+                <div className="hidden-xs">{submitButton}</div>
+              </div>
+              <div className="visible-xs mt-2">
+                <div className="d-flex justify-content-end">
+                  { this.state.errorMessage && errorMessage }
+                  <div>{submitButton}</div>
                 </div>
               </div>
             </div>
           </div>
-          )
-        }
-
+        </div>
       </div>
     );
   }

+ 33 - 40
src/client/js/components/PageComment/CommentEditorLazyRenderer.jsx

@@ -14,7 +14,6 @@ class CommentEditorLazyRenderer extends React.Component {
 
     this.state = {
       isEditorShown: false,
-      isLayoutTypeGrowi: false,
     };
 
     this.growiRenderer = this.props.appContainer.getRenderer('comment');
@@ -22,15 +21,6 @@ class CommentEditorLazyRenderer extends React.Component {
     this.showCommentFormBtnClickHandler = this.showCommentFormBtnClickHandler.bind(this);
   }
 
-  componentWillMount() {
-    this.init();
-  }
-
-  init() {
-    const layoutType = this.props.appContainer.getConfig().layoutType;
-    this.setState({ isLayoutTypeGrowi: layoutType === 'crowi-plus' || layoutType === 'growi' });
-  }
-
   showCommentFormBtnClickHandler() {
     this.setState({ isEditorShown: !this.state.isEditorShown });
   }
@@ -38,48 +28,51 @@ class CommentEditorLazyRenderer extends React.Component {
   render() {
     const { appContainer } = this.props;
     const username = appContainer.me;
+    const isLoggedIn = username != null;
     const user = appContainer.findUser(username);
-    const isLayoutTypeGrowi = this.state.isLayoutTypeGrowi;
+
+    const layoutType = this.props.appContainer.getConfig().layoutType;
+    const isBaloonStyle = layoutType.match(/crowi-plus|growi|kibela/);
+
+    if (!isLoggedIn) {
+      return <React.Fragment></React.Fragment>;
+    }
+
     return (
       <React.Fragment>
-        { !this.state.isEditorShown
-          && (
+
+        { !this.state.isEditorShown && (
           <div className="form page-comment-form">
-            { username
-              && (
-                <div className="comment-form">
-                  { isLayoutTypeGrowi
-                  && (
-                    <div className="comment-form-user">
-                      <UserPicture user={user} />
-                    </div>
-                  )
-                  }
-                  <div className="comment-form-main">
-                    <button
-                      type="button"
-                      className={`btn btn-lg ${this.state.isLayoutTypeGrowi ? 'btn-link' : 'btn-primary'} center-block`}
-                      onClick={this.showCommentFormBtnClickHandler}
-                    >
-                      <i className="icon-bubble"></i> Add Comment
-                    </button>
-                  </div>
+            <div className="comment-form">
+              { isBaloonStyle && (
+                <div className="comment-form-user">
+                  <UserPicture user={user} />
                 </div>
-              )
-            }
+              ) }
+              <div className="comment-form-main">
+                { !this.state.isEditorShown && (
+                  <button
+                    type="button"
+                    className={`btn btn-lg ${isBaloonStyle ? 'btn-link' : 'btn-primary'} center-block`}
+                    onClick={this.showCommentFormBtnClickHandler}
+                  >
+                    <i className="icon-bubble"></i> Add Comment
+                  </button>
+                ) }
+              </div>
+            </div>
           </div>
-          )
-        }
-        { this.state.isEditorShown
-          && (
+        ) }
+
+        { this.state.isEditorShown && (
           <CommentEditor
             growiRenderer={this.growiRenderer}
             replyTo={undefined}
             commentButtonClickedHandler={this.showCommentFormBtnClickHandler}
           >
           </CommentEditor>
-)
-        }
+        ) }
+
       </React.Fragment>
     );
   }

+ 61 - 81
src/client/js/components/PageComments.jsx

@@ -31,8 +31,6 @@ class PageComments extends React.Component {
     super(props);
 
     this.state = {
-      isLayoutTypeGrowi: false,
-
       // for deleting comment
       commentToDelete: undefined,
       isDeleteConfirmModalShown: false,
@@ -61,9 +59,6 @@ class PageComments extends React.Component {
       return;
     }
 
-    const layoutType = this.props.appContainer.getConfig().layoutType;
-    this.setState({ isLayoutTypeGrowi: layoutType === 'crowi-plus' || layoutType === 'growi' });
-
     this.props.commentContainer.retrieveComments();
   }
 
@@ -110,10 +105,10 @@ class PageComments extends React.Component {
     });
   }
 
-  // adds replies to specific comment object
-  addRepliesToComments(comment, replies) {
+  // get replies to specific comment object
+  getRepliesFor(comment, allReplies) {
     const replyList = [];
-    replies.forEach((reply) => {
+    allReplies.forEach((reply) => {
       if (reply.replyTo === comment._id) {
         replyList.push(reply);
       }
@@ -122,102 +117,87 @@ class PageComments extends React.Component {
   }
 
   /**
-   * generate Elements of Comment
+   * render Elements of Comment Thread
    *
-   * @param {any} comments Array of Comment Model Obj
+   * @param {any} comment Comment Model Obj
+   * @param {any} replies List of Reply Comment Model Obj
    *
    * @memberOf PageComments
    */
-  generateCommentElements(comments, replies) {
-    return comments.map((comment) => {
-
-      const commentId = comment._id;
-      const showEditor = this.state.showEditorIds.has(commentId);
-      const username = this.props.appContainer.me;
-
-      const replyList = this.addRepliesToComments(comment, replies);
-
-      return (
-        <div key={commentId}>
-          <Comment
-            comment={comment}
-            deleteBtnClicked={this.confirmToDeleteComment}
-            growiRenderer={this.growiRenderer}
-            replyList={replyList}
-          />
-          <div className="container-fluid">
-            <div className="row">
-              <div className="col-xs-offset-1 col-xs-11 col-sm-offset-1 col-sm-11 col-md-offset-1 col-md-11 col-lg-offset-1 col-lg-11">
-                { !showEditor && (
-                  <div>
-                    { username
-                    && (
-                      <div className="col-xs-offset-6 col-sm-offset-6 col-md-offset-6 col-lg-offset-6">
-                        <Button
-                          bsStyle="primary"
-                          className="fcbtn btn btn-outline btn-rounded btn-xxs"
-                          onClick={() => { return this.replyButtonClickedHandler(commentId) }}
-                        >
-                          Reply <i className="fa fa-mail-reply"></i>
-                        </Button>
-                      </div>
-                    )
-                  }
-                  </div>
-                )}
-                { showEditor && (
-                  <CommentEditor
-                    growiRenderer={this.growiRenderer}
-                    replyTo={commentId}
-                    commentButtonClickedHandler={this.commentButtonClickedHandler}
-                  />
-                )}
-              </div>
-            </div>
+  renderThread(comment, replies) {
+    const commentId = comment._id;
+    const showEditor = this.state.showEditorIds.has(commentId);
+    const isLoggedIn = this.props.appContainer.me != null;
+
+    let rootClassNames = 'page-comment-thread';
+    if (replies.length === 0) {
+      rootClassNames += ' page-comment-thread-no-replies';
+    }
+
+    return (
+      <div key={commentId} className={`mb-5 ${rootClassNames}`}>
+        <Comment
+          comment={comment}
+          deleteBtnClicked={this.confirmToDeleteComment}
+          growiRenderer={this.growiRenderer}
+          replyList={replies}
+        />
+        { !showEditor && isLoggedIn && (
+          <div className="text-right">
+            <Button
+              bsStyle="default"
+              className="btn btn-outline btn-default btn-sm btn-comment-reply"
+              onClick={() => { return this.replyButtonClickedHandler(commentId) }}
+            >
+              <i className="icon-fw icon-action-redo"></i> Reply
+            </Button>
           </div>
-          <br />
-        </div>
-      );
-    });
+        )}
+        { showEditor && isLoggedIn && (
+          <div className="page-comment-reply-form">
+            <CommentEditor
+              growiRenderer={this.growiRenderer}
+              replyTo={commentId}
+              commentButtonClickedHandler={this.commentButtonClickedHandler}
+            />
+          </div>
+        )}
+      </div>
+    );
   }
 
   render() {
-    const currentComments = [];
-    const currentReplies = [];
+    const topLevelComments = [];
+    const allReplies = [];
+
+    const layoutType = this.props.appContainer.getConfig().layoutType;
+    const isBaloonStyle = layoutType.match(/crowi-plus|growi|kibela/);
 
     let comments = this.props.commentContainer.state.comments;
-    if (this.state.isLayoutTypeGrowi) {
+    if (isBaloonStyle) {
       // replace with asc order array
       comments = comments.slice().reverse(); // non-destructive reverse
     }
 
     comments.forEach((comment) => {
       if (comment.replyTo === undefined) {
-      // comment is not a reply
-        currentComments.push(comment);
+        // comment is not a reply
+        topLevelComments.push(comment);
       }
       else {
-      // comment is a reply
-        currentReplies.push(comment);
+        // comment is a reply
+        allReplies.push(comment);
       }
     });
 
-    // generate elements
-    const currentElements = this.generateCommentElements(currentComments, currentReplies);
-
-    // generate blocks
-    const currentBlock = (
-      <div className="page-comments-list-current" id="page-comments-list-current">
-        {currentElements}
-      </div>
-    );
-
-    // layout blocks
-    const commentsElements = (<div>{currentBlock}</div>);
-
     return (
       <div>
-        {commentsElements}
+        { topLevelComments.map((topLevelComment) => {
+          // get related replies
+          const replies = this.getRepliesFor(topLevelComment, allReplies);
+
+          return this.renderThread(topLevelComment, replies);
+        }) }
 
         <DeleteCommentModal
           isShown={this.state.isDeleteConfirmModalShown}

+ 1 - 1
src/client/js/services/AppContainer.js

@@ -29,7 +29,7 @@ export default class AppContainer extends Container {
 
     const body = document.querySelector('body');
 
-    this.me = body.dataset.currentUsername;
+    this.me = body.dataset.currentUsername || null; // will be initialized with null when data is empty string
     this.isAdmin = body.dataset.isAdmin === 'true';
     this.csrfToken = body.dataset.csrftoken;
     this.isPluginEnabled = body.dataset.pluginEnabled === 'true';

+ 9 - 14
src/client/styles/scss/_comment.scss

@@ -23,12 +23,9 @@
 
 .main-container {
   .page-comments {
-    .page-comments-list-toggle-newer,
     .page-comments-list-toggle-older {
-      display: block;
-      margin: 8px;
+      display: inline-block;
       font-size: 0.9em;
-      text-align: center;
     }
 
     .page-comment {
@@ -43,16 +40,14 @@
           opacity: 1;
         }
       }
+
+      .page-comment-meta {
+        display: flex;
+        justify-content: flex-end;
+
+        font-size: 0.9em;
+        color: #999;
+      }
     }
   }
 }
-
-.btn-xxs {
-  display: inline-flex;
-  align-items: center;
-  justify-content: center;
-  width: 50px;
-  height: 10px;
-  font-size: 11px;
-  border-radius: 1px;
-}

+ 5 - 0
src/client/styles/scss/_comment_crowi.scss

@@ -0,0 +1,5 @@
+.crowi.main-container {
+  .page-comment-main {
+    margin-bottom: 0.5em;
+  }
+}

+ 28 - 9
src/client/styles/scss/_comment_growi.scss

@@ -2,8 +2,8 @@
   %comment-section {
     position: relative;
     padding: 1em;
-    margin-bottom: 1em;
     margin-left: 4.5em;
+
     // screen-xs
     @media (max-width: $screen-xs) {
       margin-left: 3.5em;
@@ -75,14 +75,32 @@
       margin-bottom: 0.5em;
       word-wrap: break-word;
     }
+  }
 
-    .page-comment-meta {
-      display: flex;
-      justify-content: flex-end;
-
-      font-size: 0.9em;
-      color: #999;
-    }
+  /*
+   * reply
+   */
+  .page-comment-reply {
+    margin-top: 1em;
+  }
+  // remove margin after hidden replies
+  .page-comments-hidden-replies + .page-comment-reply {
+    margin-top: 0;
+  }
+  .page-comment-reply,
+  .page-comment-reply-form {
+    margin-right: 15px;
+    margin-left: 6em;
+  }
+  // reply button
+  .btn.btn-comment-reply {
+    width: 120px;
+    margin-top: 0.5em;
+    margin-right: 15px;
+
+    border-top: none;
+    border-right: none;
+    border-left: none;
   }
 
   // show when hover
@@ -97,7 +115,8 @@
     }
 
     position: relative;
-    margin-top: 2em;
+    margin-top: 1em;
+
     // user icon
     .picture {
       @extend %picture;

+ 62 - 20
src/client/styles/scss/_comment_kibela.scss

@@ -3,18 +3,19 @@
   %comment-section {
     position: relative;
     padding: 1em;
-    margin-bottom: 1em; // screen-xs
     margin-left: 4.5em;
+
     @media (max-width: $screen-xs) {
       margin-left: 3.5em;
-    } // speech balloon
+    }
+
+    // speech balloon
     &:before {
       position: absolute;
       top: 1.5em;
       left: -1em;
       display: block;
       width: 0;
-      width: 0; // screen-xs
       height: 0;
       content: '';
       border-top: 20px solid transparent;
@@ -22,11 +23,13 @@
       border-bottom: 20px solid transparent;
       border-left: 20px solid transparent;
       border-left-width: 0;
+
       @media (max-width: $screen-xs) {
         top: 1em;
       }
     }
   }
+
   %picture {
     float: left;
     width: 3em;
@@ -37,54 +40,91 @@
       height: 2em;
     }
   }
+
   .page-comments-row {
     margin: 10px 0px;
   }
+
   .page-comments {
     h4 {
       margin-bottom: 1em;
     }
   }
   .page-comment {
-    position: relative; // ユーザー名
+    position: relative;
+
+    // ユーザー名
     .page-comment-creator {
       margin-top: -0.5em;
       margin-bottom: 0.5em;
       font-weight: bold;
-    } // ユーザーアイコン
+    }
+
+    // ユーザーアイコン
     .picture {
       @extend %picture;
-    } // コメントセクション
+    }
+
+    // コメントセクション
     .page-comment-main {
       @extend %comment-section;
       background: #e6e9ec;
       border-radius: 0.35em;
-    } // コメント本文
+    }
+
+    // コメント本文
     .page-comment-body {
       margin-bottom: 0.5em;
       word-wrap: break-word;
     }
-    .page-comment-meta {
-      display: flex;
-      justify-content: flex-end;
+  }
 
-      font-size: 0.9em;
-      color: #999;
-    }
-  } // show when hover
+  /*
+   * reply
+   */
+  .page-comment-reply {
+    margin-top: 1em;
+  }
+  // remove margin after hidden replies
+  .page-comments-hidden-replies + .page-comment-reply {
+    margin-top: 0;
+  }
+  .page-comment-reply,
+  .page-comment-reply-form {
+    margin-right: 15px;
+    margin-left: 6em;
+  }
+  // reply button
+  .btn.btn-comment-reply {
+    width: 120px;
+    margin-top: 0.5em;
+    margin-right: 15px;
+
+    border-top: none;
+    border-right: none;
+    border-left: none;
+  }
+
+  // show when hover
   .page-comment-main:hover > .page-comment-control {
     display: block;
-  } // display cheatsheet for comment form only
+  }
+
+  // display cheatsheet for comment form only
   .comment-form {
-    position: relative;
-    margin-top: 2em; // user icon
-    border: none;
     .editor-cheatsheet {
       display: none;
     }
+
+    position: relative;
+    margin-top: 1em;
+
+    // user icon
     .picture {
       @extend %picture;
-    } // seciton
+    }
+
+    // seciton
     .comment-form-main {
       @extend %comment-section;
       background: #e6e9ec;
@@ -92,7 +132,9 @@
       .CodeMirror {
         border: 0px;
       }
-    } // textarea
+    }
+
+    // textarea
     .comment-write {
       margin-bottom: 0.5em;
     }

+ 1 - 0
src/client/styles/scss/style-app.scss

@@ -15,6 +15,7 @@
 @import 'admin';
 @import 'attachments';
 @import 'comment';
+@import 'comment_crowi';
 @import 'comment_growi';
 @import 'comment_kibela';
 @import 'navbar_kibela';