Taichi Masuyama 4 лет назад
Родитель
Сommit
c45cde2472

+ 43 - 7
packages/app/src/components/PageEditor/InAppNotificationDropdown.tsx

@@ -1,5 +1,7 @@
 import React, { useState, useEffect, FC } from 'react';
-import { Dropdown, DropdownToggle, DropdownMenu } from 'reactstrap';
+import {
+  Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
+} from 'reactstrap';
 import PropTypes from 'prop-types';
 import { withUnstatedContainers } from '../UnstatedUtils';
 // import DropdownMenu from './InAppNotificationDropdown/DropdownMenu';
@@ -9,7 +11,6 @@ import SocketIoContainer from '~/client/services/SocketIoContainer';
 
 
 const InAppNotificationDropdown: FC = (props) => {
-  console.log('propsHoge', props);
 
   const [count, setCount] = useState(0);
   const [isLoaded, setIsLoaded] = useState(false);
@@ -22,9 +23,6 @@ const InAppNotificationDropdown: FC = (props) => {
     // fetchNotificationStatus();
   }, []);
 
-  /**
-    * TODO: Listen to socket on the client side by GW-7402
-    */
   const initializeSocket = (props) => {
     console.log(props);
 
@@ -111,16 +109,54 @@ const InAppNotificationDropdown: FC = (props) => {
 
   const badge = count > 0 ? <span className="badge badge-pill badge-danger notification-badge">{count}</span> : '';
 
+
+  const RenderUnLoadedInAppNotification = (): JSX.Element => {
+    return (
+      <i className="fa fa-spinner"></i>
+    );
+  };
+
+  const RenderEmptyInAppNotification = (): JSX.Element => {
+    return (
+      // TODO: apply i18n by GW-7536
+      <>You had no notifications, yet.</>
+    );
+  };
+
+  // TODO: improve renderInAppNotificationList by GW-7535
+  // refer to https://github.com/crowi/crowi/blob/eecf2bc821098d2516b58104fe88fae81497d3ea/client/components/Notification/Notification.tsx
+  const RenderInAppNotificationList = (): JSX.Element => {
+    // notifications.map((notification) =>
+    return (
+      // <Notification key={notification._id} notification={notification} onClick={notificationClickHandler} />)
+      <>fuga</>
+    );
+  };
+
+  function renderInAppNotificationContents(): JSX.Element {
+    if (isLoaded === false) {
+      return <RenderUnLoadedInAppNotification />;
+    }
+    if (notifications.length === 0) {
+      return <RenderEmptyInAppNotification />;
+    }
+    return <RenderInAppNotificationList />;
+  }
+
   return (
     <Dropdown className="notification-wrapper" isOpen={isOpen} toggle={toggleDropdownHandler}>
       <DropdownToggle tag="a" className="nav-link">
         <i className="icon-bell mr-2"></i>
         {badge}
       </DropdownToggle>
-      <DropdownMenu>hoge</DropdownMenu>
+      <DropdownMenu right>
+        {renderInAppNotificationContents}
+        <DropdownItem divider />
+        {/* TODO: Able to show all notifications by GW-7534 */}
+        <a>See All</a>
+      </DropdownMenu>
     </Dropdown>
   );
-
 };
 
 /**

+ 20 - 1
packages/app/src/server/crowi/index.js

@@ -64,6 +64,8 @@ function Crowi() {
   this.interceptorManager = new InterceptorManager();
   this.slackIntegrationService = null;
   this.inAppNotificationService = null;
+  this.ActivityService = null;
+  this.CommentService = null;
   this.xss = new Xss();
 
   this.tokens = null;
@@ -122,6 +124,8 @@ Crowi.prototype.init = async function() {
     this.setupImport(),
     this.setupPageService(),
     this.setupInAppNotificationService(),
+    this.setupActivityService(),
+    this.setupCommentService(),
     this.setupSyncPageStatusService(),
   ]);
 
@@ -160,7 +164,8 @@ Crowi.prototype.initForTest = async function() {
     // this.setupExport(),
     // this.setupImport(),
     this.setupPageService(),
-    this.setupInAppNotificationService(),
+    // this.setupInAppNotificationService(),
+    // this.setupActivityService(),
   ]);
 
   // globalNotification depends on slack and mailer
@@ -648,6 +653,20 @@ Crowi.prototype.setupInAppNotificationService = async function() {
   }
 };
 
+Crowi.prototype.setupActivityService = async function() {
+  const ActivityService = require('../service/activity');
+  if (this.activityService == null) {
+    this.activityService = new ActivityService(this);
+  }
+};
+
+Crowi.prototype.setupCommentService = async function() {
+  const CommentService = require('../service/comment');
+  if (this.commentService == null) {
+    this.commentService = new CommentService(this);
+  }
+};
+
 Crowi.prototype.setupSyncPageStatusService = async function() {
   const SyncPageStatusService = require('../service/system-events/sync-page-status');
   if (this.syncPageStatusService == null) {

+ 13 - 0
packages/app/src/server/events/activity.ts

@@ -0,0 +1,13 @@
+import { EventEmitter } from 'events';
+import loggerFactory from '../../utils/logger';
+
+const logger = loggerFactory('growi:events:activity');
+
+
+export default class ActivityEvent extends EventEmitter {
+
+  onRemove(action: string, activity: any): void {
+    logger.info('onRemove activity event fired');
+  }
+
+}

+ 4 - 0
packages/app/src/server/events/comment.ts

@@ -22,5 +22,9 @@ CommentEvent.prototype.onUpdate = function() {
   logger.info('onUpdate comment event fired');
 };
 
+CommentEvent.prototype.onRemove = function() {
+  logger.info('onRemove comment event fired');
+};
+
 
 module.exports = CommentEvent;

+ 6 - 21
packages/app/src/server/models/activity.ts

@@ -9,7 +9,7 @@ import ActivityDefine from '../util/activityDefine';
 
 import Watcher from './watcher';
 // import { InAppNotification } from './in-app-notification';
-// import activityEvent from '../events/activity';
+import ActivityEvent from '../events/activity';
 
 const logger = loggerFactory('growi:models:activity');
 
@@ -39,8 +39,6 @@ export interface ActivityModel extends Model<ActivityDocument> {
   getActionUsersFromActivities(activities: ActivityDocument[]): any[]
 }
 
-// const activityEvent = crowi.event('Activity');
-
 // TODO: add revision id
 const activitySchema = new Schema<ActivityDocument, ActivityModel>({
   user: {
@@ -94,8 +92,9 @@ activitySchema.statics.createByParameters = function(parameters) {
    * @param {object} parameters
    */
 activitySchema.statics.removeByParameters = async function(parameters) {
+  const activityEvent = new ActivityEvent();
   const activity = await this.findOne(parameters);
-  // activityEvent.emit('remove', activity);
+  activityEvent.emit('remove', activity);
 
   return this.deleteMany(parameters).exec();
 };
@@ -151,11 +150,14 @@ activitySchema.statics.removeByPageUnlike = function(page, user) {
 
 /**
    * @param {Page} page
+   *
    * @return {Promise}
    */
 activitySchema.statics.removeByPage = async function(page) {
+  // const activityEvent = new ActivityEvent();
   const activities = await this.find({ target: page });
   for (const activity of activities) {
+    // TODO: implement removeActivity when page deleted by GW-7481
     // activityEvent.emit('remove', activity);
   }
   return this.deleteMany({ target: page }).exec();
@@ -209,22 +211,5 @@ activitySchema.post('save', async(savedActivity: ActivityDocument) => {
   }
 });
 
-
-/**
- * TODO: improve removeActivity that decleard in InAppNotificationService by GW-7481
- */
-
-// because mongoose's 'remove' hook fired only when remove by a method of Document (not by a Model method)
-// move 'save' hook from mongoose's events to activityEvent if I have a time.
-// activityEvent.on('remove', async(activity: ActivityDocument) => {
-
-//   try {
-//     await InAppNotification.removeActivity(activity);
-//   }
-//   catch (err) {
-//     logger.error(err);
-//   }
-// });
-
 const Activity = getOrCreateModel<ActivityDocument, ActivityModel>('Activity', activitySchema);
 export { Activity };

+ 26 - 21
packages/app/src/server/models/comment.js

@@ -80,26 +80,37 @@ module.exports = function(crowi) {
     return commentData;
   };
 
-  commentSchema.statics.removeCommentsByPageId = function(pageId) {
-    const Comment = this;
 
-    return new Promise(((resolve, reject) => {
-      Comment.remove({ page: pageId }, (err, done) => {
-        if (err) {
-          return reject(err);
-        }
+  /**
+   * post remove hook
+   */
+  commentSchema.post('reomove', async(savedComment) => {
+    const Page = crowi.model('Page');
+    const commentEvent = crowi.event('comment');
 
-        resolve(done);
-      });
-    }));
-  };
+    try {
+      // TODO: move Page.updateCommentCount to commentService by GW7532
+      const page = await Page.updateCommentCount(savedComment.page);
+      debug('CommentCount Updated', page);
+    }
+    catch (err) {
+      throw err;
+    }
 
-  commentSchema.methods.removeWithReplies = async function() {
+    await commentEvent.emit('remove', savedComment);
+  });
+
+  commentSchema.methods.removeWithReplies = async function(comment) {
     const Comment = crowi.model('Comment');
-    return Comment.remove({
+    const commentEvent = crowi.event('comment');
+
+    await Comment.remove({
       $or: (
         [{ replyTo: this._id }, { _id: this._id }]),
     });
+
+    await commentEvent.emit('remove', comment);
+    return;
   };
 
   commentSchema.statics.findCreatorsByPage = async function(page) {
@@ -114,6 +125,7 @@ module.exports = function(crowi) {
     const commentEvent = crowi.event('comment');
 
     try {
+      // TODO: move Page.updateCommentCount to commentService by GW7532
       const page = await Page.updateCommentCount(savedComment.page);
       debug('CommentCount Updated', page);
     }
@@ -121,14 +133,7 @@ module.exports = function(crowi) {
       throw err;
     }
 
-    await commentEvent.emit('create', savedComment.creator);
-    try {
-      const activityLog = await Activity.createByPageComment(savedComment);
-      debug('Activity created', activityLog);
-    }
-    catch (err) {
-      throw err;
-    }
+    await commentEvent.emit('create', savedComment);
   });
 
   return mongoose.model('Comment', commentSchema);

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

@@ -8,7 +8,6 @@ import loggerFactory from '../../utils/logger';
 import { Activity, ActivityDocument } from './activity';
 
 const logger = loggerFactory('growi:models:inAppNotification');
-const User = getModelSafely('User') || require('~/server/models/user')();
 
 const STATUS_UNREAD = 'UNREAD';
 const STATUS_UNOPENED = 'UNOPENED';
@@ -31,9 +30,9 @@ export interface InAppNotificationModel extends Model<InAppNotificationDocument>
   upsertByActivity(userIds: Types.ObjectId[], activity: ActivityDocument, createdAt?: Date | null): Promise<InAppNotificationDocument | null>
   // commented out type 'Query' temporary to avoid ts error
   removeEmpty()/* : Query<any> */
-  read(user: typeof User) /* : Promise<Query<any>> */
+  read(user) /* : Promise<Query<any>> */
 
-  open(user: typeof User, id: Types.ObjectId): Promise<InAppNotificationDocument | null>
+  open(user, id: Types.ObjectId): Promise<InAppNotificationDocument | null>
   getUnreadCountByUser(user: Types.ObjectId): Promise<number | undefined>
 
   STATUS_UNREAD: string

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

@@ -439,7 +439,7 @@ module.exports = function(crowi, app) {
         throw new Error('Current user is not accessible to this page.');
       }
 
-      await comment.removeWithReplies();
+      await comment.removeWithReplies(comment);
       await Page.updateCommentCount(comment.page);
     }
     catch (err) {

+ 43 - 0
packages/app/src/server/service/activity.ts

@@ -0,0 +1,43 @@
+import loggerFactory from '../../utils/logger';
+
+import { Activity } from '../models/activity';
+
+import ActivityDefine from '../util/activityDefine';
+
+
+const logger = loggerFactory('growi:service:ActivityService');
+
+class ActivityService {
+
+  crowi: any;
+
+  inAppNotificationService: any;
+
+  // commentEvent!: any;
+
+  constructor(crowi) {
+    this.crowi = crowi;
+    this.inAppNotificationService = crowi.inAppNotificationService;
+  }
+
+  /**
+   * @param {Comment} comment
+   * @return {Promise}
+   */
+  removeByPageCommentDelete = async function(comment) {
+    const parameters = await {
+      user: comment.creator,
+      targetModel: ActivityDefine.MODEL_PAGE,
+      target: comment.page,
+      eventModel: ActivityDefine.MODEL_COMMENT,
+      event: comment._id,
+      action: ActivityDefine.ACTION_COMMENT,
+    };
+
+    await Activity.removeByParameters(parameters);
+    return;
+  };
+
+}
+
+module.exports = ActivityService;

+ 68 - 0
packages/app/src/server/service/comment.ts

@@ -0,0 +1,68 @@
+import loggerFactory from '../../utils/logger';
+
+import { Activity } from '../models/activity';
+
+const InAppNotificationService = require('./in-app-notification');
+const ActivityService = require('./activity');
+
+const logger = loggerFactory('growi:service:CommentService');
+
+
+class CommentService {
+
+  crowi!: any;
+
+  commentEvent!: any;
+
+  constructor(crowi: any) {
+    this.crowi = crowi;
+
+    this.commentEvent = crowi.event('comment');
+
+    // init
+    this.initCommentEvent();
+  }
+
+
+  initCommentEvent(): void {
+    // create
+    this.commentEvent.on('create', async(savedComment) => {
+      this.commentEvent.onCreate();
+
+      try {
+        const activityLog = await Activity.createByPageComment(savedComment);
+        logger.info('Activity created', activityLog);
+      }
+      catch (err) {
+        throw err;
+      }
+
+    });
+
+    // update
+    this.commentEvent.on('update', (user) => {
+      this.commentEvent.onUpdate();
+      const { inAppNotificationService } = this.crowi;
+
+      inAppNotificationService.emitSocketIo(user);
+    });
+
+    // remove
+    this.commentEvent.on('remove', async(comment) => {
+      this.commentEvent.onRemove();
+
+      const { activityService } = this.crowi;
+
+      try {
+        // TODO: Able to remove child activities of comment by GW-7510
+        await activityService.removeByPageCommentDelete(comment);
+      }
+      catch (err) {
+        logger.error(err);
+      }
+    });
+  }
+
+}
+
+module.exports = CommentService;

+ 10 - 28
packages/app/src/server/service/in-app-notification.ts

@@ -1,5 +1,11 @@
 import Crowi from '../crowi';
 import { InAppNotification } from '~/server/models/in-app-notification';
+import { Activity } from '~/server/models/activity';
+
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:service:inAppNotification');
+
 
 class InAppNotificationService {
 
@@ -13,39 +19,15 @@ class InAppNotificationService {
   constructor(crowi: Crowi) {
     this.crowi = crowi;
     this.socketIoService = crowi.socketIoService;
-    this.commentEvent = crowi.event('comment');
-
-    // init
-    this.initCommentEvent();
   }
 
-  initCommentEvent(): void {
-    this.commentEvent.on('create', (user) => {
-      this.commentEvent.onCreate();
-    });
-
-    this.commentEvent.on('update', (user) => {
-      this.commentEvent.onUpdate();
-
-      if (this.socketIoService.isInitialized) {
-        this.socketIoService.getDefaultSocket().emit('comment updated', { user });
-      }
-    });
 
+  emitSocketIo = async(user) => {
+    if (this.socketIoService.isInitialized) {
+      await this.socketIoService.getDefaultSocket().emit('comment updated', { user });
+    }
   }
 
-  removeActivity = async function(activity) {
-    const { _id, target, action } = activity;
-    const query = { target, action };
-    const parameters = { $pull: { activities: _id } };
-
-    const result = await InAppNotification.updateMany(query, parameters);
-
-    await InAppNotification.removeEmpty();
-
-    return result;
-  };
-
 }
 
 module.exports = InAppNotificationService;