Преглед изворни кода

Merge pull request #2926 from weseek/feat/refactor-like-bookmark

feat/GW-3875
Yuki Takei пре 5 година
родитељ
комит
de36ab9fa4

+ 21 - 45
src/client/js/components/BookmarkButton.jsx

@@ -2,49 +2,22 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import { toastError } from '../util/apiNotification';
 import { toastError } from '../util/apiNotification';
+import { withUnstatedContainers } from './UnstatedUtils';
+import PageContainer from '../services/PageContainer';
 
 
 class BookmarkButton extends React.Component {
 class BookmarkButton extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
-    this.state = {
-      isBookmarked: false,
-      sumOfBookmarks: 0,
-    };
-
     this.handleClick = this.handleClick.bind(this);
     this.handleClick = this.handleClick.bind(this);
   }
   }
 
 
-  async componentDidMount() {
-    const { pageId, crowi } = this.props;
-    // if guest user
-    if (!this.isUserLoggedIn()) {
-      // do nothing
-      return;
-    }
-
-    try {
-      const response = await crowi.apiv3.get('/bookmarks', { pageId });
-      if (response.data.bookmark != null) {
-        this.setState({ isBookmarked: true });
-      }
-      const result = await crowi.apiv3.get('/bookmarks/count-bookmarks', { pageId });
-      this.setState({ sumOfBookmarks: result.data.sumOfBookmarks });
-    }
-    catch (err) {
-      toastError(err);
-    }
-
-  }
-
   async handleClick() {
   async handleClick() {
-    const { crowi, pageId } = this.props;
-    const bool = !this.state.isBookmarked;
+    const { pageContainer } = this.props;
 
 
     try {
     try {
-      await crowi.apiv3.put('/bookmarks', { pageId, bool });
-      this.setState({ isBookmarked: bool });
+      pageContainer.toggleBookmark();
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -56,32 +29,35 @@ class BookmarkButton extends React.Component {
   }
   }
 
 
   render() {
   render() {
+    const { pageContainer } = this.props;
     // if guest user
     // if guest user
     if (!this.isUserLoggedIn()) {
     if (!this.isUserLoggedIn()) {
       return <div></div>;
       return <div></div>;
     }
     }
 
 
     return (
     return (
-      <div className="d-flex">
-        <button
-          type="button"
-          onClick={this.handleClick}
-          className={`btn rounded-circle btn-bookmark border-0
+      <button
+        type="button"
+        onClick={this.handleClick}
+        className={`btn btn-bookmark border-0
           ${`btn-${this.props.size}`}
           ${`btn-${this.props.size}`}
-          ${this.state.isBookmarked ? 'active' : ''}`}
-        >
-          <i className="icon-star"></i>
-        </button>
-        <div className="total-bookmarks">
-          {this.state.sumOfBookmarks}
-        </div>
-      </div>
+          ${pageContainer.state.isBookmarked ? 'active' : ''}`}
+      >
+        <i className="icon-star mr-3"></i>
+        <span className="total-bookmarks">
+          {pageContainer.state.sumOfBookmarks}
+        </span>
+      </button>
     );
     );
   }
   }
 
 
 }
 }
 
 
+const BookmarkButtonWrapper = withUnstatedContainers(BookmarkButton, [PageContainer]);
+
 BookmarkButton.propTypes = {
 BookmarkButton.propTypes = {
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+
   pageId: PropTypes.string,
   pageId: PropTypes.string,
   crowi: PropTypes.object.isRequired,
   crowi: PropTypes.object.isRequired,
   size: PropTypes.string,
   size: PropTypes.string,
@@ -91,4 +67,4 @@ BookmarkButton.defaultProps = {
   size: 'md',
   size: 'md',
 };
 };
 
 
-export default BookmarkButton;
+export default BookmarkButtonWrapper;

+ 12 - 14
src/client/js/components/LikeButton.jsx

@@ -4,25 +4,20 @@ import PropTypes from 'prop-types';
 import { toastError } from '../util/apiNotification';
 import { toastError } from '../util/apiNotification';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 import AppContainer from '../services/AppContainer';
 import AppContainer from '../services/AppContainer';
+import PageContainer from '../services/PageContainer';
 
 
 class LikeButton extends React.Component {
 class LikeButton extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
-    this.state = {
-      isLiked: props.isLiked,
-    };
-
     this.handleClick = this.handleClick.bind(this);
     this.handleClick = this.handleClick.bind(this);
   }
   }
 
 
   async handleClick() {
   async handleClick() {
-    const { appContainer, pageId } = this.props;
-    const bool = !this.state.isLiked;
+    const { pageContainer } = this.props;
     try {
     try {
-      await appContainer.apiv3.put('/page/likes', { pageId, bool });
-      this.setState({ isLiked: bool });
+      pageContainer.toggleLike();
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -34,6 +29,7 @@ class LikeButton extends React.Component {
   }
   }
 
 
   render() {
   render() {
+    const { pageContainer } = this.props;
     // if guest user
     // if guest user
     if (!this.isUserLoggedIn()) {
     if (!this.isUserLoggedIn()) {
       return <div></div>;
       return <div></div>;
@@ -43,10 +39,13 @@ class LikeButton extends React.Component {
       <button
       <button
         type="button"
         type="button"
         onClick={this.handleClick}
         onClick={this.handleClick}
-        className={`btn rounded-circle btn-like border-0
-        ${this.state.isLiked ? 'active' : ''}`}
+        className={`btn btn-like border-0 d-edit-none
+        ${pageContainer.state.isLiked ? 'active' : ''}`}
       >
       >
-        <i className="icon-like"></i>
+        <i className="icon-like mr-3"></i>
+        <span className="total-likes">
+          {pageContainer.state.sumOfLikers}
+        </span>
       </button>
       </button>
     );
     );
   }
   }
@@ -56,13 +55,12 @@ class LikeButton extends React.Component {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const LikeButtonWrapper = withUnstatedContainers(LikeButton, [AppContainer]);
+const LikeButtonWrapper = withUnstatedContainers(LikeButton, [AppContainer, PageContainer]);
 
 
 LikeButton.propTypes = {
 LikeButton.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
 
-  pageId: PropTypes.string,
-  isLiked: PropTypes.bool,
   size: PropTypes.string,
   size: PropTypes.string,
 };
 };
 
 

+ 3 - 6
src/client/js/components/Navbar/GrowiSubNavigation.jsx

@@ -114,7 +114,7 @@ const UserInfo = ({ pageUser }) => {
 const PageReactionButtons = ({ appContainer, pageContainer }) => {
 const PageReactionButtons = ({ appContainer, pageContainer }) => {
 
 
   const {
   const {
-    pageId, isLiked, pageUser, sumOfLikers,
+    pageId, isLiked, pageUser,
   } = pageContainer.state;
   } = pageContainer.state;
 
 
   return (
   return (
@@ -124,10 +124,7 @@ const PageReactionButtons = ({ appContainer, pageContainer }) => {
         <LikeButton pageId={pageId} isLiked={isLiked} />
         <LikeButton pageId={pageId} isLiked={isLiked} />
       </span>
       </span>
       )}
       )}
-      <span className="mr-2 total-likes">
-        {sumOfLikers}
-      </span>
-      <span className="mr-2">
+      <span>
         <BookmarkButton pageId={pageId} crowi={appContainer} />
         <BookmarkButton pageId={pageId} crowi={appContainer} />
       </span>
       </span>
     </>
     </>
@@ -190,7 +187,7 @@ const GrowiSubNavigation = (props) => {
       {/* Right side */}
       {/* Right side */}
       <div className="d-flex">
       <div className="d-flex">
 
 
-        <div className="d-flex flex-column align-items-end justify-content-center">
+        <div className="d-flex flex-column align-items-end">
           <div className="d-flex">
           <div className="d-flex">
             { !isPageInTrash && !isPageNotFound && !isPageForbidden && <PageReactionButtons appContainer={appContainer} pageContainer={pageContainer} /> }
             { !isPageInTrash && !isPageNotFound && !isPageForbidden && <PageReactionButtons appContainer={appContainer} pageContainer={pageContainer} /> }
             { !isPageNotFound && !isPageForbidden && <PageManagement /> }
             { !isPageNotFound && !isPageForbidden && <PageManagement /> }

+ 36 - 14
src/client/js/services/PageContainer.js

@@ -48,15 +48,14 @@ export default class PageContainer extends Container {
       revisionCreatedAt: +mainContent.getAttribute('data-page-revision-created'),
       revisionCreatedAt: +mainContent.getAttribute('data-page-revision-created'),
       path,
       path,
       tocHtml: '',
       tocHtml: '',
-      isLiked: JSON.parse(mainContent.getAttribute('data-page-is-liked')),
-
-      seenUserIds: mainContent.getAttribute('data-page-ids-of-seen-users'),
+      isLiked: false,
+      isBookmarked: false,
       seenUsers: [],
       seenUsers: [],
       countOfSeenUsers: mainContent.getAttribute('data-page-count-of-seen-users'),
       countOfSeenUsers: mainContent.getAttribute('data-page-count-of-seen-users'),
 
 
       likerUsers: [],
       likerUsers: [],
       sumOfLikers: 0,
       sumOfLikers: 0,
-
+      sumOfBookmarks: 0,
       createdAt: mainContent.getAttribute('data-page-created-at'),
       createdAt: mainContent.getAttribute('data-page-created-at'),
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
       isForbidden:  JSON.parse(mainContent.getAttribute('data-page-is-forbidden')),
       isForbidden:  JSON.parse(mainContent.getAttribute('data-page-is-forbidden')),
@@ -155,20 +154,43 @@ export default class PageContainer extends Container {
 
 
   async initStateOthers() {
   async initStateOthers() {
 
 
-    const likerListElem = document.getElementById('liker-list');
-    if (likerListElem != null) {
-      const { userIdsStr, sumOfLikers } = likerListElem.dataset;
-      this.setState({ sumOfLikers });
+    this.retrieveLikeInfo();
+    this.retrieveBookmarkInfo();
+    this.checkAndUpdateImageUrlCached(this.state.likerUsers);
+  }
 
 
-      if (userIdsStr === '') {
-        return;
-      }
+  async retrieveLikeInfo() {
+    const like = await this.appContainer.apiv3Get('/page/like-info', { _id: this.state.pageId });
+    this.setState({
+      sumOfLikers: like.data.sumOfLikers,
+      likerUsers: like.data.users.liker,
+      isLiked: like.data.isLiked,
+    });
+  }
+
+  async toggleLike() {
+    const bool = !this.state.isLiked;
+    await this.appContainer.apiv3Put('/page/likes', { pageId: this.state.pageId, bool });
+    this.setState({ isLiked: bool });
 
 
-      const { users } = await this.appContainer.apiGet('/users.list', { user_ids: userIdsStr });
-      this.setState({ likerUsers: users });
+    return this.retrieveLikeInfo();
+  }
 
 
-      this.checkAndUpdateImageUrlCached(users);
+  async retrieveBookmarkInfo() {
+    const response = await this.appContainer.apiv3Get('/bookmarks', { pageId: this.state.pageId });
+    if (response.data.bookmarks != null) {
+      this.setState({ isBookmarked: true });
     }
     }
+    else {
+      this.setState({ isBookmarked: false });
+    }
+    this.setState({ sumOfBookmarks: response.data.sumOfBookmarks });
+  }
+
+  async toggleBookmark() {
+    const bool = !this.state.isBookmarked;
+    await this.appContainer.apiv3Put('/bookmarks', { pageId: this.state.pageId, bool });
+    return this.retrieveBookmarkInfo();
   }
   }
 
 
   async checkAndUpdateImageUrlCached(users) {
   async checkAndUpdateImageUrlCached(users) {

+ 1 - 0
src/client/styles/scss/_override-bootstrap-variables.scss

@@ -39,6 +39,7 @@ $line-height-base: 1.42857;
 $text-muted: $gray-500;
 $text-muted: $gray-500;
 $blockquote-small-color: $gray-500;
 $blockquote-small-color: $gray-500;
 
 
+
 //== Components
 //== Components
 //
 //
 $border-radius:               .15rem;
 $border-radius:               .15rem;

+ 6 - 20
src/client/styles/scss/_subnav.scss

@@ -35,23 +35,15 @@
 
 
   .btn-like,
   .btn-like,
   .btn-bookmark {
   .btn-bookmark {
-    width: 40px;
     height: 40px;
     height: 40px;
     font-size: 20px;
     font-size: 20px;
+    border-radius: $border-radius-xl;
   }
   }
 
 
-  .total-likes {
-    width: 8px;
-    height: 16px;
-    padding: 0.5em 0 0 0;
-    font-size: 16px;
-  }
-
+  .total-likes,
   .total-bookmarks {
   .total-bookmarks {
-    width: 8px;
-    height: 16px;
-    padding: 0.5em 0 0 0;
-    font-size: 16px;
+    font-size: 17px;
+    font-weight: $font-weight-bold;
   }
   }
 
 
   ul.authors {
   ul.authors {
@@ -94,19 +86,13 @@
     .btn-bookmark {
     .btn-bookmark {
       @extend .btn-sm;
       @extend .btn-sm;
 
 
-      width: 30px;
       height: 30px;
       height: 30px;
       font-size: 15px !important;
       font-size: 15px !important;
+      border-radius: $border-radius-xl;
     }
     }
 
 
-    .total-likes {
-      width: 6px;
-      height: 12px;
-      font-size: 12px;
-    }
-
+    .total-likes,
     .total-bookmarks {
     .total-bookmarks {
-      width: 6px;
       height: 12px;
       height: 12px;
       font-size: 12px;
       font-size: 12px;
     }
     }

+ 5 - 19
src/server/routes/apiv3/bookmarks.js

@@ -66,7 +66,7 @@ module.exports = (crowi) => {
       body('pageId').isMongoId(),
       body('pageId').isMongoId(),
       body('bool').isBoolean(),
       body('bool').isBoolean(),
     ],
     ],
-    countBookmarks: [
+    bookmarkInfo: [
       query('pageId').isMongoId(),
       query('pageId').isMongoId(),
     ],
     ],
   };
   };
@@ -94,12 +94,13 @@ module.exports = (crowi) => {
    *                schema:
    *                schema:
    *                  $ref: '#/components/schemas/Bookmark'
    *                  $ref: '#/components/schemas/Bookmark'
    */
    */
-  router.get('/', accessTokenParser, loginRequired, async(req, res) => {
+  router.get('/', accessTokenParser, loginRequired, validator.bookmarkInfo, async(req, res) => {
     const { pageId } = req.query;
     const { pageId } = req.query;
 
 
     try {
     try {
-      const bookmark = await Bookmark.findByPageIdAndUserId(pageId, req.user);
-      return res.apiv3({ bookmark });
+      const bookmarks = await Bookmark.findByPageIdAndUserId(pageId, req.user);
+      const sumOfBookmarks = await Bookmark.countByPageId(pageId);
+      return res.apiv3({ bookmarks, sumOfBookmarks });
     }
     }
     catch (err) {
     catch (err) {
       logger.error('get-bookmark-failed', err);
       logger.error('get-bookmark-failed', err);
@@ -179,20 +180,5 @@ module.exports = (crowi) => {
    *                  $ref: '#/components/schemas/Bookmark'
    *                  $ref: '#/components/schemas/Bookmark'
    */
    */
 
 
-
-  router.get('/count-bookmarks', accessTokenParser, loginRequired, validator.countBookmarks, apiV3FormValidator, async(req, res) => {
-    const { pageId } = req.query;
-
-    try {
-      const sumOfBookmarks = await Bookmark.countByPageId(pageId);
-      return res.apiv3({ sumOfBookmarks });
-    }
-    catch (err) {
-      logger.error('get-bookmarks-list-failed', err);
-      return res.apiv3Err(err, 500);
-    }
-  });
-
-
   return router;
   return router;
 };
 };

+ 21 - 1
src/server/routes/apiv3/page.js

@@ -117,7 +117,7 @@ module.exports = (crowi) => {
   const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
   const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
 
 
   const globalNotificationService = crowi.getGlobalNotificationService();
   const globalNotificationService = crowi.getGlobalNotificationService();
-  const { Page, GlobalNotificationSetting } = crowi.models;
+  const { Page, GlobalNotificationSetting, User } = crowi.models;
   const { exportService } = crowi;
   const { exportService } = crowi;
 
 
   const validator = {
   const validator = {
@@ -125,6 +125,9 @@ module.exports = (crowi) => {
       body('pageId').isString(),
       body('pageId').isString(),
       body('bool').isBoolean(),
       body('bool').isBoolean(),
     ],
     ],
+    likeInfo: [
+      query('_id').isMongoId(),
+    ],
     export: [
     export: [
       query('format').isString().isIn(['md', 'pdf']),
       query('format').isString().isIn(['md', 'pdf']),
       query('revisionId').isString(),
       query('revisionId').isString(),
@@ -196,6 +199,23 @@ module.exports = (crowi) => {
     return res.apiv3({ result });
     return res.apiv3({ result });
   });
   });
 
 
+  router.get('/like-info', loginRequired, validator.likeInfo, async(req, res) => {
+    const pageId = req.query._id;
+    const userId = req.user._id;
+    try {
+      const page = await Page.findById(pageId);
+      const users = await Page.findById(pageId).populate('liker', User.USER_PUBLIC_FIELDS);
+      const sumOfLikers = page.liker.length;
+      const isLiked = page.liker.includes(userId);
+
+      return res.apiv3({ users, sumOfLikers, isLiked });
+    }
+    catch (err) {
+      logger.error('error like info', err);
+      return res.apiv3Err(err, 500);
+    }
+  });
+
   /**
   /**
   * @swagger
   * @swagger
   *
   *

+ 7 - 0
src/server/views/layout-growi/widget/liker-and-seenusers.html

@@ -0,0 +1,7 @@
+<div class="liker-and-seenusers">
+  <div
+    id="seen-user-list"
+    data-user-ids-str="{{ page.seenUsers|slice(-15)|default([])|reverse|join(',') }}"
+    data-sum-of-seen-users="{{ page.seenUsers.length|default(0) }}"
+  ></div>
+</div>