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

Merge branch 'feat/notification' into fix/80942-hit-api-multi-times-when-page-scrolled

kaori 4 лет назад
Родитель
Сommit
dbcb5624ee

+ 3 - 1
packages/app/src/components/FormattedDistanceDate.jsx

@@ -14,7 +14,7 @@ const FormattedDistanceDate = (props) => {
   const dateFormatted = format(date, 'yyyy/MM/dd HH:mm');
 
   const diff = Math.abs(differenceInSeconds(date, baseDate));
-  if (diff > props.differenceForAvoidingFormat) {
+  if (!props.isNotShowDate && diff > props.differenceForAvoidingFormat) {
     return <>{dateFormatted}</>;
   }
 
@@ -35,10 +35,12 @@ FormattedDistanceDate.propTypes = {
   // the number(sec) from 'baseDate' to avoid format
   differenceForAvoidingFormat: PropTypes.number,
   isShowTooltip: PropTypes.bool,
+  isNotShowDate: PropTypes.bool,
 };
 FormattedDistanceDate.defaultProps = {
   differenceForAvoidingFormat: 86400 * 3,
   isShowTooltip: true,
+  isNotShowDate: false,
 };
 
 export default FormattedDistanceDate;

+ 33 - 4
packages/app/src/components/InAppNotification/AllInAppNotifications.tsx

@@ -1,15 +1,44 @@
-import React, { FC } from 'react';
+import React, { FC, useState } from 'react';
 
 import InAppNotificationList from './InAppNotificationList';
 import { useSWRxInAppNotifications } from '../../stores/in-app-notification';
+import PaginationWrapper from '../PaginationWrapper';
 
 
 const AllInAppNotifications: FC = () => {
-  const limit = 6;
-  const { data: inAppNotificationData } = useSWRxInAppNotifications(limit);
+  const [activePage, setActivePage] = useState(1);
+  const [offset, setOffset] = useState(0);
+  const limit = 10;
+  const { data: inAppNotificationData } = useSWRxInAppNotifications(limit, offset);
+
+  if (inAppNotificationData == null) {
+    return (
+      <div className="wiki">
+        <div className="text-muted text-center">
+          <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
+        </div>
+      </div>
+    );
+  }
+
+  const setPageNumber = (selectedPageNumber): void => {
+    setActivePage(selectedPageNumber);
+    const offset = (selectedPageNumber - 1) * limit;
+    setOffset(offset);
+  };
 
   return (
-    <InAppNotificationList inAppNotificationData={inAppNotificationData} />
+    <>
+      <InAppNotificationList inAppNotificationData={inAppNotificationData} />
+      <PaginationWrapper
+        activePage={activePage}
+        changePage={setPageNumber}
+        totalItemsCount={inAppNotificationData.totalDocs}
+        pagingLimit={inAppNotificationData.limit}
+        align="center"
+        size="sm"
+      />
+    </>
   );
 };
 

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

@@ -23,7 +23,7 @@ const InAppNotificationDropdown: FC<Props> = (props: Props) => {
   const [count, setCount] = useState(0);
   const [isOpen, setIsOpen] = useState(false);
   const limit = 6;
-  const { data: inAppNotificationData } = useSWRxInAppNotifications(limit);
+  const { data: inAppNotificationData, mutate } = useSWRxInAppNotifications(limit);
 
   useEffect(() => {
     initializeSocket(props);
@@ -59,17 +59,19 @@ const InAppNotificationDropdown: FC<Props> = (props: Props) => {
   };
 
   const toggleDropdownHandler = () => {
-    if (isOpen === false && count > 0) {
+    if (!isOpen && count > 0) {
       updateNotificationStatus();
     }
 
     const newIsOpenState = !isOpen;
+    if (newIsOpenState) {
+      mutate();
+    }
     setIsOpen(newIsOpenState);
   };
 
   const badge = count > 0 ? <span className="badge badge-pill badge-danger grw-notification-badge">{count}</span> : '';
 
-
   return (
     <Dropdown className="notification-wrapper" isOpen={isOpen} toggle={toggleDropdownHandler}>
       <DropdownToggle tag="a">

+ 6 - 2
packages/app/src/components/InAppNotification/InAppNotificationElm.tsx

@@ -73,16 +73,20 @@ const InAppNotificationElm = (props: Props): JSX.Element => {
 
   const actionType: string = notification.action;
   let actionMsg: string;
+  let actionIcon: string;
 
   switch (actionType) {
     case 'PAGE_UPDATE':
       actionMsg = 'updated on';
+      actionIcon = 'ti-agenda';
       break;
     case 'COMMENT_CREATE':
       actionMsg = 'commented on';
+      actionIcon = 'icon-bubble';
       break;
     default:
       actionMsg = '';
+      actionIcon = '';
   }
 
 
@@ -96,8 +100,8 @@ const InAppNotificationElm = (props: Props): JSX.Element => {
           <div>
             <b>{actionUsers}</b> {actionMsg} <PagePathLabel page={pagePath} />
           </div>
-          <i className="fa fa-file-o mr-2" />
-          <FormattedDistanceDate id={notification._id} date={notification.createdAt} isShowTooltip={false} />
+          <i className={`${actionIcon} mr-2`} />
+          <FormattedDistanceDate id={notification._id} date={notification.createdAt} isShowTooltip={false} isNotShowDate />
         </div>
       </div>
     </div>

+ 23 - 29
packages/app/src/components/PaginationWrapper.jsx → packages/app/src/components/PaginationWrapper.tsx

@@ -1,18 +1,21 @@
-import React, { useCallback, useMemo } from 'react';
-import PropTypes from 'prop-types';
+import React, {
+  FC, memo, useCallback, useMemo,
+} from 'react';
 
 import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
 
-/**
- *
- * @author Mikitaka Itizawa <itizawa@weseek.co.jp>
- *
- * @export
- * @class PaginationWrapper
- * @extends {React.Component}
- */
 
-const PaginationWrapper = React.memo((props) => {
+type Props = {
+  activePage: number,
+  changePage?: (number) => void,
+  totalItemsCount: number,
+  pagingLimit?: number,
+  align?: string,
+  size?: string,
+};
+
+
+const PaginationWrapper: FC<Props> = memo((props: Props) => {
   const {
     activePage, changePage, totalItemsCount, pagingLimit, align,
   } = props;
@@ -59,14 +62,14 @@ const PaginationWrapper = React.memo((props) => {
    * this function set << & <
    */
   const generateFirstPrev = useCallback(() => {
-    const paginationItems = [];
+    const paginationItems: JSX.Element[] = [];
     if (activePage !== 1) {
       paginationItems.push(
         <PaginationItem key="painationItemFirst">
-          <PaginationLink first onClick={() => { return changePage(1) }} />
+          <PaginationLink first onClick={() => { return changePage != null && changePage(1) }} />
         </PaginationItem>,
         <PaginationItem key="painationItemPrevious">
-          <PaginationLink previous onClick={() => { return changePage(activePage - 1) }} />
+          <PaginationLink previous onClick={() => { return changePage != null && changePage(activePage - 1) }} />
         </PaginationItem>,
       );
     }
@@ -89,11 +92,11 @@ const PaginationWrapper = React.memo((props) => {
    * this function set  numbers
    */
   const generatePaginations = useCallback(() => {
-    const paginationItems = [];
+    const paginationItems: JSX.Element[] = [];
     for (let number = paginationStart; number <= maxViewPageNum; number++) {
       paginationItems.push(
         <PaginationItem key={`paginationItem-${number}`} active={number === activePage}>
-          <PaginationLink onClick={() => { return changePage(number) }}>
+          <PaginationLink onClick={() => { return changePage != null && changePage(number) }}>
             {number}
           </PaginationLink>
         </PaginationItem>,
@@ -108,14 +111,14 @@ const PaginationWrapper = React.memo((props) => {
    * this function set > & >>
    */
   const generateNextLast = useCallback(() => {
-    const paginationItems = [];
+    const paginationItems: JSX.Element[] = [];
     if (totalPage !== activePage) {
       paginationItems.push(
         <PaginationItem key="painationItemNext">
-          <PaginationLink next onClick={() => { return changePage(activePage + 1) }} />
+          <PaginationLink next onClick={() => { return changePage != null && changePage(activePage + 1) }} />
         </PaginationItem>,
         <PaginationItem key="painationItemLast">
-          <PaginationLink last onClick={() => { return changePage(totalPage) }} />
+          <PaginationLink last onClick={() => { return changePage != null && changePage(totalPage) }} />
         </PaginationItem>,
       );
     }
@@ -133,7 +136,7 @@ const PaginationWrapper = React.memo((props) => {
   }, [activePage, changePage, totalPage]);
 
   const getListClassName = useMemo(() => {
-    const listClassNames = [];
+    const listClassNames: string[] = [];
 
     if (align === 'center') {
       listClassNames.push('justify-content-center');
@@ -157,15 +160,6 @@ const PaginationWrapper = React.memo((props) => {
 
 });
 
-PaginationWrapper.propTypes = {
-  activePage: PropTypes.number.isRequired,
-  changePage: PropTypes.func.isRequired,
-  totalItemsCount: PropTypes.number.isRequired,
-  pagingLimit: PropTypes.number,
-  align: PropTypes.string,
-  size: PropTypes.string,
-};
-
 PaginationWrapper.defaultProps = {
   align: 'left',
   size: 'md',

+ 2 - 2
packages/app/src/interfaces/in-app-notification-settings.ts

@@ -1,4 +1,4 @@
-import { Schema } from 'mongoose';
+import { Types } from 'mongoose';
 
 export enum subscribeRuleNames {
   PAGE_CREATE = 'PAGE_CREATE'
@@ -13,6 +13,6 @@ export interface ISubscribeRule {
   isEnabled: boolean;
 }
 export interface IInAppNotificationSettings {
-  userId: Schema.Types.ObjectId;
+  userId: Types.ObjectId;
   subscribeRules: ISubscribeRule[];
 }

+ 1 - 1
packages/app/src/server/models/in-app-notification-settings.ts

@@ -7,7 +7,7 @@ export interface InAppNotificationSettingsDocument extends IInAppNotificationSet
 export type InAppNotificationSettingsModel = Model<InAppNotificationSettingsDocument>
 
 const inAppNotificationSettingsSchema = new Schema<IInAppNotificationSettings>({
-  userId: { type: String },
+  userId: { type: Schema.Types.ObjectId },
   subscribeRules: [
     {
       name: { type: String, require: true, enum: subscribeRuleNames },

+ 1 - 5
packages/app/src/server/models/in-app-notification.ts

@@ -4,13 +4,9 @@ import {
 import mongoosePaginate from 'mongoose-paginate-v2';
 
 import { getOrCreateModel } from '@growi/core';
-import Activity, { ActivityDocument } from './activity';
+import { ActivityDocument } from './activity';
 import ActivityDefine from '../util/activityDefine';
 
-import loggerFactory from '../../utils/logger';
-
-const logger = loggerFactory('growi:models:inAppNotification');
-
 export const STATUS_UNREAD = 'UNREAD';
 export const STATUS_UNOPENED = 'UNOPENED';
 export const STATUS_OPENED = 'OPENED';

+ 7 - 6
packages/app/src/server/routes/apiv3/in-app-notification.ts

@@ -17,19 +17,20 @@ module.exports = (crowi) => {
   router.get('/list', accessTokenParser, loginRequiredStrictly, async(req, res) => {
     const user = req.user;
 
-    let limit = 10;
-    if (req.query.limit) {
-      limit = parseInt(req.query.limit, 10);
-    }
+    const limit = parseInt(req.query.limit) || 10;
 
     let offset = 0;
     if (req.query.offset) {
       offset = parseInt(req.query.offset, 10);
     }
 
-    const requestLimit = limit + 1;
+    const queryOptions = {
+      offset,
+      limit,
+    };
+
 
-    const paginationResult = await inAppNotificationService.getLatestNotificationsByUser(user._id, requestLimit, offset);
+    const paginationResult = await inAppNotificationService.getLatestNotificationsByUser(user._id, queryOptions);
 
 
     const getActionUsersFromActivities = function(activities) {

+ 2 - 3
packages/app/src/server/routes/apiv3/pages.js

@@ -1,7 +1,7 @@
 import { pagePathUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 
-import Subscription, { STATUS_SUBSCRIBE } from '~/server/models/subscription';
+import { subscribeRuleNames } from '~/interfaces/in-app-notification-settings';
 
 const logger = loggerFactory('growi:routes:apiv3:pages'); // eslint-disable-line no-unused-vars
 const express = require('express');
@@ -320,10 +320,9 @@ module.exports = (crowi) => {
       }
     }
 
-    // TODO: 80103
     // create subscription
     try {
-      await Subscription.subscribeByPageId(req.user._id, createdPage._id, STATUS_SUBSCRIBE);
+      await crowi.inAppNotificationService.createSubscription(req.user.id, createdPage._id, subscribeRuleNames.PAGE_CREATE);
     }
     catch (err) {
       logger.error('Failed to create subscription document', err);

+ 23 - 3
packages/app/src/server/service/in-app-notification.ts

@@ -1,10 +1,14 @@
-import { Types } from 'mongoose';
+import { Types, PaginateResult } from 'mongoose';
 import { subDays } from 'date-fns';
 import Crowi from '../crowi';
 import {
   InAppNotification, STATUS_UNREAD, STATUS_UNOPENED, STATUS_OPENED,
+  InAppNotificationDocument,
 } from '~/server/models/in-app-notification';
+
 import { ActivityDocument } from '~/server/models/activity';
+import InAppNotificationSettings from '~/server/models/in-app-notification-settings';
+import Subscription, { STATUS_SUBSCRIBE } from '~/server/models/subscription';
 
 import { IUser } from '~/interfaces/user';
 import { HasObjectId } from '~/interfaces/has-object-id';
@@ -79,15 +83,19 @@ export default class InAppNotificationService {
     return;
   }
 
-  getLatestNotificationsByUser = async(userId, limitNum, offset) => {
+  getLatestNotificationsByUser = async(
+      userId: Types.ObjectId,
+      queryOptions: {offset: number, limit: number},
+  ): Promise<PaginateResult<InAppNotificationDocument>> => {
+    const { limit, offset } = queryOptions;
 
     try {
       const paginationResult = await InAppNotification.paginate(
         { user: userId },
         {
           sort: { createdAt: -1 },
+          limit,
           offset,
-          limit: limitNum || 10,
           populate: [
             { path: 'user' },
             { path: 'target' },
@@ -135,6 +143,18 @@ export default class InAppNotificationService {
     }
   };
 
+  createSubscription = async function(userId: Types.ObjectId, pageId: Types.ObjectId, targetRuleName: string): Promise<void> {
+    const query = { userId };
+    const inAppNotificationSettings = await InAppNotificationSettings.findOne(query);
+    if (inAppNotificationSettings != null) {
+      const subscribeRule = inAppNotificationSettings.subscribeRules.find(subscribeRule => subscribeRule.name === targetRuleName);
+      if (subscribeRule != null && subscribeRule.isEnabled) {
+        await Subscription.subscribeByPageId(userId, pageId, STATUS_SUBSCRIBE);
+      }
+    }
+
+    return;
+  };
 
 }
 

+ 3 - 3
packages/app/src/stores/in-app-notification.ts

@@ -7,11 +7,11 @@ import { IInAppNotification } from '../interfaces/in-app-notification';
 
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 export const useSWRxInAppNotifications = <Data, Error>(
-  // TODO: apply pagination by 80107
   limit: number,
+  offset?: number,
 ): SWRResponse<PaginateResult<IInAppNotification>, Error> => {
   return useSWR(
-    '/in-app-notification/list',
-    endpoint => apiv3Get(endpoint, { limit }).then(response => response.data),
+    ['/in-app-notification/list', limit, offset],
+    endpoint => apiv3Get(endpoint, { limit, offset }).then(response => response.data),
   );
 };