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

Merge branch 'feat/notification' into feat/78834-fetch-in-app-notification-status

Shun Miyazawa 4 лет назад
Родитель
Сommit
348d1e2d77

+ 3 - 2
packages/app/package.json

@@ -55,8 +55,8 @@
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@google-cloud/storage": "^5.8.5",
     "@growi/plugin-attachment-refs": "^4.4.3-RC.0",
-    "@growi/plugin-pukiwiki-like-linker": "^4.4.3-RC.0",
     "@growi/plugin-lsx": "^4.4.3-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^4.4.3-RC.0",
     "@growi/slack": "^4.4.3-RC.0",
     "@promster/express": "^5.0.1",
     "@promster/server": "^6.0.0",
@@ -96,7 +96,6 @@
     "graceful-fs": "^4.1.11",
     "growi-commons": "^5.0.4",
     "helmet": "^4.6.0",
-    "nocache": "^3.0.1",
     "http-errors": "~1.6.2",
     "i18next": "^20.3.2",
     "i18next-express-middleware": "^2.0.0",
@@ -114,6 +113,7 @@
     "mongoose-unique-validator": "^2.0.3",
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
+    "nocache": "^3.0.1",
     "nodemailer": "^6.6.2",
     "nodemailer-ses-transport": "~1.5.0",
     "openid-client": "=2.5.0",
@@ -157,6 +157,7 @@
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
+    "@types/mongoose-paginate-v2": "1.3.9",
     "@types/multer": "^1.4.5",
     "@types/react-dom": "^17.0.9",
     "autoprefixer": "^9.0.0",

+ 2 - 2
packages/app/src/components/InAppNotification/InAppNotificationDropdown.tsx

@@ -80,10 +80,10 @@ const InAppNotificationDropdown: FC = (props) => {
     */
 
   const fetchNotificationList = async(props) => {
-    console.log('propsappContainerHoge', props.appContainer);
     const limit = 6;
     try {
-      const notifications = await props.appContainer.apiv3Get('/in-app-notification/list', { limit });
+      const inAppNotificationList = await props.appContainer.apiv3Get('/in-app-notification/list', { limit });
+
       // setNotifications(notifications);
       // setIsLoaded(true);
     }

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

@@ -10,34 +10,32 @@ import LikeButton from '../LikeButton';
 import SubscribeButton from '../SubscribeButton';
 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>
-          <BookmarkButton />
+          <LikeButton />
         </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 isViewMode = editorMode === 'view';
@@ -46,7 +44,7 @@ const SubnavButtons = (props) => {
     <>
       {isViewMode && (
         <>
-          { pageContainer.isAbleToShowPageReactionButtons && <PageReactionButtons appContainer={appContainer} pageContainer={pageContainer} /> }
+          { pageContainer.isAbleToShowPageReactionButtons && <PageReactionButtons pageContainer={pageContainer} /> }
           { 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 { UncontrolledTooltip } from 'reactstrap';
@@ -13,11 +15,14 @@ type Props = {
   pageId: string,
 };
 
-const SubscruibeButton: FC<Props> = (props: Props) => {
+const SubscribeButton: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
 
   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() => {
     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 (
     <>
       <button
         type="button"
         id="subscribe-button"
         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>
 
       {appContainer.isGuestUser && (
@@ -60,5 +89,5 @@ const SubscruibeButton: FC<Props> = (props: Props) => {
 /**
  * Wrapper component for using unstated
  */
-const SubscruibeButtonWrapper = withUnstatedContainers(SubscruibeButton, [AppContainer, PageContainer]);
-export default SubscruibeButtonWrapper;
+const SubscribeButtonWrapper = withUnstatedContainers(SubscribeButton, [AppContainer, PageContainer]);
+export default SubscribeButtonWrapper;

+ 9 - 18
packages/app/src/server/models/in-app-notification.ts

@@ -1,7 +1,9 @@
 import {
-  Types, Document, Model, Schema /* , Query */,
+  Types, Document, PaginateModel, Schema, /* , Query */
 } from 'mongoose';
+import mongoosePaginate from 'mongoose-paginate-v2';
 import ActivityDefine from '../util/activityDefine';
+import { ActivityDocument } from './activity';
 import { getOrCreateModel } from '../util/mongoose-utils';
 import loggerFactory from '../../utils/logger';
 
@@ -18,13 +20,15 @@ export interface InAppNotificationDocument extends Document {
   targetModel: string
   target: Types.ObjectId
   action: string
-  activities: Types.ObjectId[]
+  activities: ActivityDocument[]
   status: string
   createdAt: Date
 }
 
-export interface InAppNotificationModel extends Model<InAppNotificationDocument> {
-  findLatestInAppNotificationsByUser(user: Types.ObjectId, skip: number, offset: number): Promise<InAppNotificationDocument[]>
+
+export interface InAppNotificationModel extends PaginateModel<InAppNotificationDocument> {
+  findLatestInAppNotificationsByUser(user: Types.ObjectId, skip: number, offset: number)
+  getUnreadCountByUser(user: Types.ObjectId): Promise<number | undefined>
   open(user, id: Types.ObjectId): Promise<InAppNotificationDocument | null>
   read(user) /* : Promise<Query<any>> */
 
@@ -73,6 +77,7 @@ const inAppNotificationSchema = new Schema<InAppNotificationDocument, InAppNotif
     default: Date.now,
   },
 });
+inAppNotificationSchema.plugin(mongoosePaginate);
 
 const transform = (doc, ret) => {
   // delete ret.activities
@@ -83,20 +88,6 @@ inAppNotificationSchema.index({
   user: 1, target: 1, action: 1, createdAt: 1,
 });
 
-inAppNotificationSchema.statics.findLatestInAppNotificationsByUser = async function(user, limitNum, offset) {
-  const limit = limitNum || 10;
-
-  // TODO: improve populate refer to GROWI way by #78756
-  const notificatins = await InAppNotification.find({ user });
-  // .sort({ createdAt: -1 })
-  // .skip(offset)
-  // .limit(limit)
-  // .populate(['user', 'target'])
-  // .populate({ path: 'activities', populate: { path: 'user' } })
-  // .exec();
-  return notificatins;
-};
-
 inAppNotificationSchema.statics.STATUS_UNOPENED = function() {
   return STATUS_UNOPENED;
 };

+ 1 - 0
packages/app/src/server/models/index.js

@@ -1,4 +1,5 @@
 module.exports = {
+  Activity: require('./activity'),
   Page: require('./page'),
   // TODO GW-2746 bulk export pages
   // PageArchive: require('./page-archive'),

+ 4 - 31
packages/app/src/server/routes/apiv3/in-app-notification.ts

@@ -9,10 +9,9 @@ module.exports = (crowi) => {
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const csrf = require('../../middlewares/csrf')(crowi);
+  const inAppNotificationService = crowi.inAppNotificationService;
 
-  const { inAppNotificationService } = crowi;
-
-  router.get('/list', accessTokenParser, loginRequiredStrictly, (req, res) => {
+  router.get('/list', accessTokenParser, loginRequiredStrictly, async(req, res) => {
     const user = req.user;
 
     let limit = 10;
@@ -27,35 +26,9 @@ module.exports = (crowi) => {
 
     const requestLimit = limit + 1;
 
+    const latestInAppNotificationList = await inAppNotificationService.getLatestNotificationsByUser(user._id, requestLimit, offset);
+    return latestInAppNotificationList;
 
-    /**
-     * TODO: GW-7482
-     *   -  Replace then/catch to async/awai
-     *   -  Use mongoose-paginate-v2 for paging
-     */
-    InAppNotification.findLatestInAppNotificationsByUser(user._id, requestLimit, offset)
-      .then((notifications) => {
-        let hasPrev = false;
-        if (offset > 0) {
-          hasPrev = true;
-        }
-
-        let hasNext = false;
-        if (notifications.length > limit) {
-          hasNext = true;
-        }
-
-        const result = {
-          notifications: notifications.slice(0, limit),
-          hasPrev,
-          hasNext,
-        };
-
-        return res.apiv3(result);
-      })
-      .catch((err) => {
-        return res.apiv3Err(err);
-      });
   });
 
   router.get('/status', accessTokenParser, loginRequiredStrictly, async(req, res) => {

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

@@ -166,6 +166,9 @@ module.exports = (crowi) => {
       body('pageId').isString(),
       body('status').isBoolean(),
     ],
+    subscribeStatus: [
+      query('pageId').isString(),
+    ],
   };
 
   /**
@@ -475,8 +478,8 @@ module.exports = (crowi) => {
    *      put:
    *        tags: [Page]
    *        summary: /page/subscribe
-   *        description: Update subscribe status
-   *        operationId: updateSubscribeStatus
+   *        description: Update subscription status
+   *        operationId: updateSubscriptionStatus
    *        requestBody:
    *          content:
    *            application/json:
@@ -486,7 +489,7 @@ module.exports = (crowi) => {
    *                    $ref: '#/components/schemas/Page/properties/_id'
    *        responses:
    *          200:
-   *            description: Succeeded to update subscribe status.
+   *            description: Succeeded to update subscription status.
    *            content:
    *              application/json:
    *                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;
 };

+ 58 - 0
packages/app/src/server/service/in-app-notification.ts

@@ -73,6 +73,64 @@ export default class InAppNotificationService {
     return;
   }
 
+  getLatestNotificationsByUser = async(userId, limitNum, offset) => {
+
+    try {
+      const pagenatedInAppNotifications = await InAppNotification.paginate(
+        { user: userId },
+        {
+          sort: { createdAt: -1 },
+          offset,
+          limit: limitNum || 10,
+          populate: [
+            { path: 'user' },
+            { path: 'target' },
+            { path: 'activities', populate: { path: 'user' } },
+          ],
+        },
+      );
+    }
+    catch (err) {
+      logger.error('Error', err);
+      throw new Error(err);
+    }
+
+    try {
+      /**
+       * TODO: return results including notifications,hasPrev and hasNext by #78991
+       * refer to https://github.com/crowi/crowi/blob/eecf2bc821098d2516b58104fe88fae81497d3ea/lib/controllers/notification.ts
+       */
+      // Notification.findLatestNotificationsByUser(user._id, requestLimit, offset)
+      // .then(function (notifications) {
+      //   let hasPrev = false
+      //   if (offset > 0) {
+      //     hasPrev = true
+      //   }
+
+      //   let hasNext = false
+      //   if (notifications.length > limit) {
+      //     hasNext = true
+      //   }
+
+      //   const result = {
+      //     notifications: notifications.slice(0, limit),
+      //     hasPrev: hasPrev,
+      //     hasNext: hasNext,
+      //   }
+
+      //   return res.json(ApiResponse.success(result))
+      // })
+      // .catch(function (err) {
+      //   return res.json(ApiResponse.error(err))
+      // })
+
+    }
+    catch (err) {
+      logger.error('Error', err);
+      throw new Error(err);
+    }
+  }
+
   // inAppNotificationSchema.virtual('actionUsers').get(function(this: InAppNotificationDocument) {
   //   const Activity = getModelSafely('Activity') || require('../models/activity')(this.crowi);
   //   return Activity.getActionUsersFromActivities((this.activities as any) as ActivityDocument[]);

+ 15 - 1
yarn.lock

@@ -2914,6 +2914,20 @@
     "@types/bson" "*"
     "@types/node" "*"
 
+"@types/mongoose-paginate-v2@1.3.9":
+  version "1.3.9"
+  resolved "https://registry.yarnpkg.com/@types/mongoose-paginate-v2/-/mongoose-paginate-v2-1.3.9.tgz#a211bf0da49473e9e1f1a65d3aacbd5d5ff0408c"
+  integrity sha512-NHqTgOZRmi7gd/IkRJ2VXo88m0efKatLFrG63VEcAB98nO6nzbeRaXPUUgEFJ2Le6vleTE0WqvAuL0gO5IQF5A==
+  dependencies:
+    "@types/mongoose" "*"
+
+"@types/mongoose@*":
+  version "5.11.97"
+  resolved "https://registry.yarnpkg.com/@types/mongoose/-/mongoose-5.11.97.tgz#80b0357f3de6807eb597262f52e49c3e13ee14d8"
+  integrity sha512-cqwOVYT3qXyLiGw7ueU2kX9noE8DPGRY6z8eUxudhXY8NZ7DMKYAxyZkLSevGfhCX3dO/AoX5/SO9lAzfjon0Q==
+  dependencies:
+    mongoose "*"
+
 "@types/multer@^1.4.5":
   version "1.4.5"
   resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.5.tgz#db0557562307e9adb6661a9500c334cd7ddd0cd9"
@@ -13305,7 +13319,7 @@ mongoose-valid8@>=1.6.18:
     lodash ">=4.17.15"
     validator ">=13.0.0"
 
-mongoose@5.12.13:
+mongoose@*, mongoose@5.12.13:
   version "5.12.13"
   resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.12.13.tgz#6707fb2f6284536bb6367c94ccc3ce85475a387d"
   integrity sha512-QGn1FCzZ8Z+mMGVg8oR2kQw4NmhLloCHsw1NqKWg3Yr7WfPzkE4pe7s9P6o5pkYGsku17n9mqMHowne7EFK/zQ==