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

Merge branch 'feat/notification' into imprv/#78995-able-to-setstate-nortification-list

# Conflicts:
#	packages/app/src/components/InAppNotification/InAppNotificationDropdown.tsx
kaori 4 лет назад
Родитель
Сommit
b2751aaaad

+ 5 - 23
packages/app/src/components/InAppNotification/InAppNotificationDropdown.tsx

@@ -4,6 +4,7 @@ import {
 } from 'reactstrap';
 } from 'reactstrap';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
+import { toastError } from '~/client/util/apiNotification';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { InAppNotification as IInAppNotification } from '../../interfaces/in-app-notification';
 import { InAppNotification as IInAppNotification } from '../../interfaces/in-app-notification';
 // import DropdownMenu from './InAppNotificationDropdown/DropdownMenu';
 // import DropdownMenu from './InAppNotificationDropdown/DropdownMenu';
@@ -23,43 +24,24 @@ const InAppNotificationDropdown: FC = (props) => {
   useEffect(() => {
   useEffect(() => {
     initializeSocket(props);
     initializeSocket(props);
     fetchNotificationList(props);
     fetchNotificationList(props);
-    // fetchNotificationStatus();
   }, []);
   }, []);
 
 
   const initializeSocket = (props) => {
   const initializeSocket = (props) => {
     console.log(props);
     console.log(props);
 
 
     const socket = props.socketIoContainer.getSocket();
     const socket = props.socketIoContainer.getSocket();
-    socket.on('comment updated', (data: { user: string }) => {
+    socket.on('commentUpdated', (data: { userId: string, count: number }) => {
       // eslint-disable-next-line no-console
       // eslint-disable-next-line no-console
       console.log('socketData', data);
       console.log('socketData', data);
 
 
-      if (props.me === data.user) {
-        // TODO: Fetch notification status by #78563
-        fetchNotificationList(props);
-
+      if (props.me === data.userId) {
         // TODO: Fetch notification list by #78557
         // TODO: Fetch notification list by #78557
-        // fetchNotificationStatus();
+        // fetchNotificationList();
+
       }
       }
     });
     });
   };
   };
 
 
-
-  /**
-    * TODO: Fetch notification status by #78563
-    */
-  // async fetchNotificationStatus() {
-  //   try {
-  //     const { count = null } = await this.props.crowi.apiGet('/notification.status');
-  //     if (count !== null && count !== this.state.count) {
-  //       this.setState({ count });
-  //     }
-  //   }
-  //   catch (err) {
-  //     // TODO: error handling
-  //   }
-  // }
-
   const updateNotificationStatus = () => {
   const updateNotificationStatus = () => {
     try {
     try {
       // await this.props.crowi.apiPost('/notification.read');
       // await this.props.crowi.apiPost('/notification.read');

+ 24 - 26
packages/app/src/components/Navbar/SubNavButtons.jsx

@@ -10,34 +10,32 @@ import LikeButton from '../LikeButton';
 import SubscribeButton from '../SubscribeButton';
 import SubscribeButton from '../SubscribeButton';
 import PageManagement from '../Page/PageManagement';
 import PageManagement from '../Page/PageManagement';
 
 
-const SubnavButtons = (props) => {
-  const {
-    appContainer, navigationContainer, pageContainer, isCompactMode,
-  } = props;
-
-  /* eslint-enable react/prop-types */
-
-  /* eslint-disable react/prop-types */
-  const PageReactionButtons = ({ pageContainer }) => {
-
-    return (
-      <>
-        <span>
-          <SubscribeButton pageId={pageContainer.state.pageId} />
-        </span>
-        {pageContainer.isAbleToShowLikeButton && (
-          <span>
-            <LikeButton />
-          </span>
-        )}
+/* eslint-disable react/prop-types */
+const PageReactionButtons = ({ pageContainer }) => {
+  return (
+    <>
+      <span>
+        <SubscribeButton pageId={pageContainer.state.pageId} />
+      </span>
+      {pageContainer.isAbleToShowLikeButton && (
         <span>
         <span>
-          <BookmarkButton />
+          <LikeButton />
         </span>
         </span>
+      )}
+      <span>
+        <BookmarkButton />
+      </span>
+
+    </>
+  );
+};
+/* eslint-disable react/prop-types */
 
 
-      </>
-    );
-  };
-  /* eslint-enable react/prop-types */
+
+const SubnavButtons = (props) => {
+  const {
+    navigationContainer, pageContainer, isCompactMode,
+  } = props;
 
 
   const { editorMode } = navigationContainer.state;
   const { editorMode } = navigationContainer.state;
   const isViewMode = editorMode === 'view';
   const isViewMode = editorMode === 'view';
@@ -46,7 +44,7 @@ const SubnavButtons = (props) => {
     <>
     <>
       {isViewMode && (
       {isViewMode && (
         <>
         <>
-          { pageContainer.isAbleToShowPageReactionButtons && <PageReactionButtons appContainer={appContainer} pageContainer={pageContainer} /> }
+          { pageContainer.isAbleToShowPageReactionButtons && <PageReactionButtons pageContainer={pageContainer} /> }
           { pageContainer.isAbleToShowPageManagement && <PageManagement isCompactMode={isCompactMode} /> }
           { pageContainer.isAbleToShowPageManagement && <PageManagement isCompactMode={isCompactMode} /> }
         </>
         </>
       )}
       )}

+ 36 - 7
packages/app/src/components/SubscribeButton.tsx

@@ -1,4 +1,6 @@
-import React, { useState, FC } from 'react';
+import React, {
+  FC, useState, useCallback, useEffect,
+} from 'react';
 
 
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
@@ -13,11 +15,14 @@ type Props = {
   pageId: string,
   pageId: string,
 };
 };
 
 
-const SubscruibeButton: FC<Props> = (props: Props) => {
+const SubscribeButton: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const { appContainer, pageId } = props;
   const { appContainer, pageId } = props;
-  const [isSubscribing, setIsSubscribing] = useState(false);
+  const [isSubscribing, setIsSubscribing] = useState<boolean | null>(null);
+
+  const buttonClass = `${isSubscribing ? 'active' : ''} ${appContainer.isGuestUser ? 'disabled' : ''}`;
+  const iconClass = isSubscribing || isSubscribing == null ? 'fa fa-eye' : 'fa fa-eye-slash';
 
 
   const handleClick = async() => {
   const handleClick = async() => {
     if (appContainer.isGuestUser) {
     if (appContainer.isGuestUser) {
@@ -36,15 +41,39 @@ const SubscruibeButton: FC<Props> = (props: Props) => {
     }
     }
   };
   };
 
 
+  const fetchSubscriptionStatus = useCallback(async() => {
+    if (appContainer.isGuestUser) {
+      return;
+    }
+
+    try {
+      const res = await appContainer.apiv3Get('page/subscribe', { pageId });
+      const { subscribing } = res.data;
+      if (subscribing == null) {
+        setIsSubscribing(null);
+      }
+      else {
+        setIsSubscribing(subscribing);
+      }
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [appContainer, pageId]);
+
+  useEffect(() => {
+    fetchSubscriptionStatus();
+  }, [fetchSubscriptionStatus]);
+
   return (
   return (
     <>
     <>
       <button
       <button
         type="button"
         type="button"
         id="subscribe-button"
         id="subscribe-button"
         onClick={handleClick}
         onClick={handleClick}
-        className={`btn btn-subscribe border-0 ${isSubscribing ? 'active' : ''}  ${appContainer.isGuestUser ? 'disabled' : ''}`}
+        className={`btn btn-subscribe border-0 ${buttonClass}`}
       >
       >
-        <i className={isSubscribing ? 'fa fa-eye' : 'fa fa-eye-slash'}></i>
+        <i className={iconClass}></i>
       </button>
       </button>
 
 
       {appContainer.isGuestUser && (
       {appContainer.isGuestUser && (
@@ -60,5 +89,5 @@ const SubscruibeButton: FC<Props> = (props: Props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const SubscruibeButtonWrapper = withUnstatedContainers(SubscruibeButton, [AppContainer, PageContainer]);
-export default SubscruibeButtonWrapper;
+const SubscribeButtonWrapper = withUnstatedContainers(SubscribeButton, [AppContainer, PageContainer]);
+export default SubscribeButtonWrapper;

+ 1 - 1
packages/app/src/server/models/comment.js

@@ -73,7 +73,7 @@ module.exports = function(crowi) {
       { $set: { comment, isMarkdown } },
       { $set: { comment, isMarkdown } },
     );
     );
 
 
-    await commentEvent.emit('update', commentData.creator);
+    await commentEvent.emit('update', commentData.creator, commentData.page);
 
 
     return commentData;
     return commentData;
   };
   };

+ 2 - 3
packages/app/src/server/routes/apiv3/in-app-notification.ts

@@ -32,10 +32,9 @@ module.exports = (crowi) => {
   });
   });
 
 
   router.get('/status', accessTokenParser, loginRequiredStrictly, async(req, res) => {
   router.get('/status', accessTokenParser, loginRequiredStrictly, async(req, res) => {
-    const user = req.user;
-
+    const userId = req.user._id;
     try {
     try {
-      const count = await InAppNotification.getUnreadCountByUser(user._id);
+      const count = await inAppNotificationService.getUnreadCountByUser(userId);
       const result = { count };
       const result = { count };
       return res.apiv3(result);
       return res.apiv3(result);
     }
     }

+ 50 - 3
packages/app/src/server/routes/apiv3/page.js

@@ -166,6 +166,9 @@ module.exports = (crowi) => {
       body('pageId').isString(),
       body('pageId').isString(),
       body('status').isBoolean(),
       body('status').isBoolean(),
     ],
     ],
+    subscribeStatus: [
+      query('pageId').isString(),
+    ],
   };
   };
 
 
   /**
   /**
@@ -475,8 +478,8 @@ module.exports = (crowi) => {
    *      put:
    *      put:
    *        tags: [Page]
    *        tags: [Page]
    *        summary: /page/subscribe
    *        summary: /page/subscribe
-   *        description: Update subscribe status
-   *        operationId: updateSubscribeStatus
+   *        description: Update subscription status
+   *        operationId: updateSubscriptionStatus
    *        requestBody:
    *        requestBody:
    *          content:
    *          content:
    *            application/json:
    *            application/json:
@@ -486,7 +489,7 @@ module.exports = (crowi) => {
    *                    $ref: '#/components/schemas/Page/properties/_id'
    *                    $ref: '#/components/schemas/Page/properties/_id'
    *        responses:
    *        responses:
    *          200:
    *          200:
-   *            description: Succeeded to update subscribe status.
+   *            description: Succeeded to update subscription status.
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
@@ -508,5 +511,49 @@ module.exports = (crowi) => {
     }
     }
   });
   });
 
 
+  /**
+   * @swagger
+   *
+   *    /page/subscribe:
+   *      get:
+   *        tags: [Page]
+   *        summary: /page/subscribe
+   *        description: Get subscription status
+   *        operationId: getSubscriptionStatus
+   *        requestBody:
+   *          content:
+   *            application/json:
+   *              schema:
+   *                properties:
+   *                  pageId:
+   *                    $ref: '#/components/schemas/Page/properties/_id'
+   *        responses:
+   *          200:
+   *            description: Succeeded to get subscription status.
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/Page'
+   *          500:
+   *            description: Internal server error.
+   */
+  router.get('/subscribe', loginRequiredStrictly, validator.subscribeStatus, apiV3FormValidator, async(req, res) => {
+    const { pageId } = req.query;
+    const userId = req.user._id;
+
+    const page = await Page.findById(pageId);
+    if (!page) throw new Error('Page not found');
+
+    try {
+      const subscription = await Subscription.findByUserIdAndTargetId(userId, pageId);
+      const subscribing = subscription ? subscription.isSubscribing() : null;
+      return res.apiv3({ subscribing });
+    }
+    catch (err) {
+      logger.error('Failed to ge subscribe status', err);
+      return res.apiv3(err, 500);
+    }
+  });
+
   return router;
   return router;
 };
 };

+ 2 - 2
packages/app/src/server/service/comment.ts

@@ -51,11 +51,11 @@ class CommentService {
     });
     });
 
 
     // update
     // update
-    this.commentEvent.on('update', (user) => {
+    this.commentEvent.on('update', (userId, pageId) => {
       this.commentEvent.onUpdate();
       this.commentEvent.onUpdate();
       const { inAppNotificationService } = this.crowi;
       const { inAppNotificationService } = this.crowi;
 
 
-      inAppNotificationService.emitSocketIo(user);
+      inAppNotificationService.emitSocketIo(userId, pageId);
     });
     });
 
 
     // remove
     // remove

+ 11 - 2
packages/app/src/server/service/in-app-notification.ts

@@ -7,6 +7,7 @@ import {
 import { ActivityDocument } from '~/server/models/activity';
 import { ActivityDocument } from '~/server/models/activity';
 
 
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
+import { RoomPrefix, getRoomNameWithId } from '../util/socket-io-helpers';
 
 
 const logger = loggerFactory('growi:service:inAppNotification');
 const logger = loggerFactory('growi:service:inAppNotification');
 
 
@@ -26,12 +27,20 @@ export default class InAppNotificationService {
     this.crowi = crowi;
     this.crowi = crowi;
     this.socketIoService = crowi.socketIoService;
     this.socketIoService = crowi.socketIoService;
     this.activityEvent = crowi.event('activity');
     this.activityEvent = crowi.event('activity');
+
+    this.getUnreadCountByUser = this.getUnreadCountByUser.bind(this);
   }
   }
 
 
 
 
-  emitSocketIo = async(user) => {
+  emitSocketIo = async(userId, pageId) => {
     if (this.socketIoService.isInitialized) {
     if (this.socketIoService.isInitialized) {
-      await this.socketIoService.getDefaultSocket().emit('comment updated', { user });
+      const count = await this.getUnreadCountByUser(userId);
+
+      // emit to the room for each page
+      await this.socketIoService.getDefaultSocket()
+        .in(getRoomNameWithId(RoomPrefix.PAGE, pageId))
+        .except(getRoomNameWithId(RoomPrefix.USER, userId))
+        .emit('commentUpdated', { userId, count });
     }
     }
   }
   }