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

Merge branch 'feat/notification' into imprv/gw7402-listen-socket-on-the-client-side

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

+ 1 - 1
packages/app/src/components/Navbar/GrowiNavbar.jsx

@@ -7,7 +7,7 @@ import { UncontrolledTooltip } from 'reactstrap';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import AppContainer from '~/client/services/AppContainer';
-import InAppNotificationDropdown from '~/components/PageEditor/InAppNotificationDropdown';
+import { InAppNotificationDropdown } from '~/components/PageEditor/InAppNotificationDropdown';
 
 
 import GrowiLogo from '../Icons/GrowiLogo';

+ 80 - 85
packages/app/src/components/PageEditor/InAppNotificationDropdown.tsx

@@ -1,52 +1,43 @@
-import React from 'react';
+import React, { useState, FC } from 'react';
 import { Dropdown, DropdownToggle, DropdownMenu } from 'reactstrap';
 // import DropdownMenu from './InAppNotificationDropdown/DropdownMenu';
-// import Icon from './Common/Icon'
 // import Crowi from 'client/util/Crowi'
 // import { Notification } from 'client/types/crowi'
 
-interface Props {
+export interface Props {
   // crowi: Crowi
   me: string
 }
 
-interface State {
-  count: number
-  loaded: boolean
-  notifications: Notification[]
-  open: boolean
-}
-
-export default class InAppNotificationDropdown extends React.Component<Props, State> {
-
-  constructor(props: Props) {
-    super(props);
-
-    this.state = {
-      count: 0,
-      loaded: false,
-      notifications: [],
-      open: false,
-    };
-  }
-
-  componentDidMount() {
-    this.initializeSocket();
-    // this.fetchList();
-    // this.fetchStatus();
-  }
-
-  // TODO: socket.on by GW-7402
-  initializeSocket() {
-    // this.props.crowi.getWebSocket().on('comment updated', (data: { user: string }) => {
-    //   if (this.props.me === data.user) {
-    // this.fetchList();
-    // this.fetchStatus();
-    //   }
-    // });
-  }
-
-  // async fetchStatus() {
+export const InAppNotificationDropdown: FC<Props> = (props: Props) => {
+
+  const [count, setCount] = useState(0);
+  const [isLoaded, setIsLoaded] = useState(false);
+  const [notifications, setNotifications] = useState([]);
+  const [isOpen, setIsOpen] = useState(false);
+
+  // // componentDidMount() {
+  // //   this.initializeSocket();
+  // //   this.fetchNotificationList();
+  // //   this.fetchNotificationStatus();
+  // // }
+
+  // /**
+  //   * TODO: Listen to socket on the client side by GW-7402
+  //   */
+  // // initializeSocket() {
+  // //   this.props.crowi.getWebSocket().on('comment updated', (data: { user: string }) => {
+  // //     if (this.props.me === data.user) {
+  // //       this.fetchNotificationList();
+  // //       this.fetchNotificationStatus();
+  // //     }
+  // //   });
+  // // }
+
+  /**
+    * TODO: Fetch notification status by GW-7473
+    */
+  // async fetchNotificationStatus() {
   //   try {
   //     const { count = null } = await this.props.crowi.apiGet('/notification.status');
   //     if (count !== null && count !== this.state.count) {
@@ -58,62 +49,66 @@ export default class InAppNotificationDropdown extends React.Component<Props, St
   //   }
   // }
 
-  async updateStatus() {
+  const updateNotificationStatus = () => {
     try {
       // await this.props.crowi.apiPost('/notification.read');
-      this.setState({ count: 0 });
+      setCount(0);
     }
     catch (err) {
       // TODO: error handling
     }
-  }
+  };
 
-  // async fetchList() {
-  //   const limit = 6;
-  //   try {
-  //     const { notifications } = await this.props.crowi.apiGet('/notification.list', { limit });
-  //     this.setState({ loaded: true, notifications });
-  //   }
-  //   catch (err) {
-  //     // TODO: error handling
-  //   }
-  // }
 
-  toggle() {
-    const { open, count } = this.state;
-    if (!open && count > 0) {
-      this.updateStatus();
+  /**
+    * TODO: Fetch notification list by GW-7473
+    */
+
+  const fetchNotificationList = async() => {
+    const limit = 6;
+    try {
+      // const { notifications } = await this.props.crowi.apiGet('/notification.list', { limit });
+      setIsLoaded(true);
+      // setNotifications(notifications);
+      // this.setState({ loaded: true, notifications });
     }
-    this.setState({ open: !open });
-  }
+    catch (err) {
+      // TODO: error handling
+    }
+  };
 
-  // async handleNotificationOnClick(notification: Notification) {
-  //   try {
-  //     await this.props.crowi.apiPost('/notification.open', { id: notification._id });
-  //     // jump to target page
-  //     window.location.href = notification.target.path;
-  //   }
-  //   catch (err) {
-  //     // TODO: error handling
-  //   }
-  // }
+  const toggleDropdownHandler = () => {
+    if (isOpen === false && count > 0) {
+      updateNotificationStatus();
+    }
+    setIsOpen(!isOpen);
+  };
 
-  render() {
-    const {
-      count, open, loaded, notifications,
-    } = this.state;
+  /**
+    * TODO: Jump to the page by clicking on the notification by GW-7472
+    */
 
-    const badge = count > 0 ? <span className="badge badge-pill badge-danger notification-badge">{count}</span> : '';
+  const handleNotificationOnClick = async(notification: Notification) => {
+    try {
+      // await this.props.crowi.apiPost('/notification.open', { id: notification._id });
+      // jump to target page
+      // window.location.href = notification.target.path;
+    }
+    catch (err) {
+      // TODO: error handling
+    }
+  };
 
-    return (
-      <Dropdown className="notification-wrapper" isOpen={open} toggle={this.toggle}>
-        <DropdownToggle tag="a" className="nav-link">
-          <i className="icon-bell mr-2"></i>
-          {/* {badge} */}
-        </DropdownToggle>
-        <DropdownMenu>hoge</DropdownMenu>
-      </Dropdown>
-    );
-  }
+  const badge = count > 0 ? <span className="badge badge-pill badge-danger notification-badge">{count}</span> : '';
 
-}
+  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>
+    </Dropdown>
+  );
+
+};

+ 5 - 1
packages/app/src/server/events/comment.ts

@@ -14,8 +14,12 @@ function CommentEvent(crowi) {
 util.inherits(CommentEvent, events.EventEmitter);
 
 
+CommentEvent.prototype.onCreate = function() {
+  logger.info('onCreate comment event fired');
+};
+
 CommentEvent.prototype.onUpdate = function() {
-  logger.info('onUpdate event fired');
+  logger.info('onUpdate comment event fired');
 };
 
 

+ 161 - 158
packages/app/src/server/models/activity.ts

@@ -1,11 +1,15 @@
 import { DeleteWriteOpResultObject } from 'mongodb';
 import {
-  Types, Document, Model, Schema, model,
+  Types, Document, Model, Schema,
 } from 'mongoose';
-import loggerFactory from '~/utils/logger';
 
+import { getOrCreateModel, getModelSafely } from '../util/mongoose-utils';
+import loggerFactory from '../../utils/logger';
 import ActivityDefine from '../util/activityDefine';
-import Crowi from '../crowi';
+
+import Watcher from './watcher';
+// import { InAppNotification } from './in-app-notification';
+// import activityEvent from '../events/activity';
 
 const logger = loggerFactory('growi:models:activity');
 
@@ -33,196 +37,195 @@ export interface ActivityModel extends Model<ActivityDocument> {
   getActionUsersFromActivities(activities: ActivityDocument[]): any[]
 }
 
-export default (crowi: Crowi) => {
-  const activityEvent = crowi.event('Activity');
-
-  // TODO: add revision id
-  const activitySchema = new Schema<ActivityDocument, ActivityModel>({
-    user: {
-      type: Schema.Types.ObjectId,
-      ref: 'User',
-      index: true,
-      require: true,
-    },
-    targetModel: {
-      type: String,
-      require: true,
-      enum: ActivityDefine.getSupportTargetModelNames(),
-    },
-    target: {
-      type: Schema.Types.ObjectId,
-      refPath: 'targetModel',
-      require: true,
-    },
-    action: {
-      type: String,
-      require: true,
-      enum: ActivityDefine.getSupportActionNames(),
-    },
-    event: {
-      type: Schema.Types.ObjectId,
-      refPath: 'eventModel',
-    },
-    eventModel: {
-      type: String,
-      enum: ActivityDefine.getSupportEventModelNames(),
-    },
-    createdAt: {
-      type: Date,
-      default: Date.now,
-    },
-  });
-  activitySchema.index({ target: 1, action: 1 });
-  activitySchema.index({
-    user: 1, target: 1, action: 1, createdAt: 1,
-  }, { unique: true });
-
-  /**
+// const activityEvent = crowi.event('Activity');
+
+// TODO: add revision id
+const activitySchema = new Schema<ActivityDocument, ActivityModel>({
+  user: {
+    type: Schema.Types.ObjectId,
+    ref: 'User',
+    index: true,
+    require: true,
+  },
+  targetModel: {
+    type: String,
+    require: true,
+    enum: ActivityDefine.getSupportTargetModelNames(),
+  },
+  target: {
+    type: Schema.Types.ObjectId,
+    refPath: 'targetModel',
+    require: true,
+  },
+  action: {
+    type: String,
+    require: true,
+    enum: ActivityDefine.getSupportActionNames(),
+  },
+  event: {
+    type: Schema.Types.ObjectId,
+    refPath: 'eventModel',
+  },
+  eventModel: {
+    type: String,
+    enum: ActivityDefine.getSupportEventModelNames(),
+  },
+  createdAt: {
+    type: Date,
+    default: Date.now,
+  },
+});
+activitySchema.index({ target: 1, action: 1 });
+activitySchema.index({
+  user: 1, target: 1, action: 1, createdAt: 1,
+}, { unique: true });
+
+/**
    * @param {object} parameters
    * @return {Promise}
    */
-  activitySchema.statics.createByParameters = function(parameters) {
-    return Activity.create(parameters);
-  };
+activitySchema.statics.createByParameters = function(parameters) {
+  return this.create(parameters);
+};
 
-  /**
+/**
    * @param {object} parameters
    */
-  activitySchema.statics.removeByParameters = async function(parameters) {
-    const activity = await Activity.findOne(parameters);
-    activityEvent.emit('remove', activity);
+activitySchema.statics.removeByParameters = async function(parameters) {
+  const activity = await this.findOne(parameters);
+  // activityEvent.emit('remove', activity);
 
-    return Activity.deleteMany(parameters).exec();
-  };
+  return this.deleteMany(parameters).exec();
+};
 
-  /**
+/**
    * @param {Comment} comment
    * @return {Promise}
    */
-  activitySchema.statics.createByPageComment = function(comment) {
-    const parameters = {
-      user: comment.creator,
-      targetModel: ActivityDefine.MODEL_PAGE,
-      target: comment.page,
-      eventModel: ActivityDefine.MODEL_COMMENT,
-      event: comment._id,
-      action: ActivityDefine.ACTION_COMMENT,
-    };
-
-    return this.createByParameters(parameters);
+activitySchema.statics.createByPageComment = function(comment) {
+  const parameters = {
+    user: comment.creator,
+    targetModel: ActivityDefine.MODEL_PAGE,
+    target: comment.page,
+    eventModel: ActivityDefine.MODEL_COMMENT,
+    event: comment._id,
+    action: ActivityDefine.ACTION_COMMENT,
   };
 
-  /**
+  return this.createByParameters(parameters);
+};
+
+/**
    * @param {Page} page
    * @param {User} user
    * @return {Promise}
    */
-  activitySchema.statics.createByPageLike = function(page, user) {
-    const parameters = {
-      user: user._id,
-      targetModel: ActivityDefine.MODEL_PAGE,
-      target: page,
-      action: ActivityDefine.ACTION_LIKE,
-    };
-
-    return this.createByParameters(parameters);
+activitySchema.statics.createByPageLike = function(page, user) {
+  const parameters = {
+    user: user._id,
+    targetModel: ActivityDefine.MODEL_PAGE,
+    target: page,
+    action: ActivityDefine.ACTION_LIKE,
   };
 
-  /**
+  return this.createByParameters(parameters);
+};
+
+/**
    * @param {Page} page
    * @param {User} user
    * @return {Promise}
    */
-  activitySchema.statics.removeByPageUnlike = function(page, user) {
-    const parameters = {
-      user,
-      targetModel: ActivityDefine.MODEL_PAGE,
-      target: page,
-      action: ActivityDefine.ACTION_LIKE,
-    };
-
-    return this.removeByParameters(parameters);
+activitySchema.statics.removeByPageUnlike = function(page, user) {
+  const parameters = {
+    user,
+    targetModel: ActivityDefine.MODEL_PAGE,
+    target: page,
+    action: ActivityDefine.ACTION_LIKE,
   };
 
-  /**
+  return this.removeByParameters(parameters);
+};
+
+/**
    * @param {Page} page
    * @return {Promise}
    */
-  activitySchema.statics.removeByPage = async function(page) {
-    const activities = await Activity.find({ target: page });
-    for (const activity of activities) {
-      activityEvent.emit('remove', activity);
-    }
-    return Activity.deleteMany({ target: page }).exec();
-  };
+activitySchema.statics.removeByPage = async function(page) {
+  const activities = await this.find({ target: page });
+  for (const activity of activities) {
+    // activityEvent.emit('remove', activity);
+  }
+  return this.deleteMany({ target: page }).exec();
+};
 
-  /**
+/**
    * @param {User} user
    * @return {Promise}
    */
-  activitySchema.statics.findByUser = function(user) {
-    return Activity.find({ user }).sort({ createdAt: -1 }).exec();
-  };
+activitySchema.statics.findByUser = function(user) {
+  return this.find({ user }).sort({ createdAt: -1 }).exec();
+};
 
-  activitySchema.statics.getActionUsersFromActivities = function(activities) {
-    return activities.map(({ user }) => user).filter((user, i, self) => self.indexOf(user) === i);
-  };
+activitySchema.statics.getActionUsersFromActivities = function(activities) {
+  return activities.map(({ user }) => user).filter((user, i, self) => self.indexOf(user) === i);
+};
 
-  activitySchema.methods.getNotificationTargetUsers = async function() {
-    const User = crowi.model('User');
-    const Watcher = crowi.model('Watcher');
-    const { user: actionUser, targetModel, target } = this;
-
-    const model: any = await this.model(targetModel).findById(target);
-    const [targetUsers, watchUsers, ignoreUsers] = await Promise.all([
-      model.getNotificationTargetUsers(),
-      Watcher.getWatchers((target as any) as Types.ObjectId),
-      Watcher.getIgnorers((target as any) as Types.ObjectId),
-    ]);
-
-    const unique = array => Object.values(array.reduce((objects, object) => ({ ...objects, [object.toString()]: object }), {}));
-    const filter = (array, pull) => {
-      const ids = pull.map(object => object.toString());
-      return array.filter(object => !ids.includes(object.toString()));
-    };
-    const notificationUsers = filter(unique([...targetUsers, ...watchUsers]), [...ignoreUsers, actionUser]);
-    const activeNotificationUsers = await User.find({
-      _id: { $in: notificationUsers },
-      status: User.STATUS_ACTIVE,
-    }).distinct('_id');
-    return activeNotificationUsers;
+activitySchema.methods.getNotificationTargetUsers = async function() {
+  const User = getModelSafely('User') || require('~/server/models/user')();
+  const { user: actionUser, targetModel, target } = this;
+
+  const model: any = await this.model(targetModel).findById(target);
+  const [targetUsers, watchUsers, ignoreUsers] = await Promise.all([
+    model.getNotificationTargetUsers(),
+    Watcher.getWatchers((target as any) as Types.ObjectId),
+    Watcher.getIgnorers((target as any) as Types.ObjectId),
+  ]);
+
+  const unique = array => Object.values(array.reduce((objects, object) => ({ ...objects, [object.toString()]: object }), {}));
+  const filter = (array, pull) => {
+    const ids = pull.map(object => object.toString());
+    return array.filter(object => !ids.includes(object.toString()));
   };
+  const notificationUsers = filter(unique([...targetUsers, ...watchUsers]), [...ignoreUsers, actionUser]);
+  const activeNotificationUsers = await User.find({
+    _id: { $in: notificationUsers },
+    status: User.STATUS_ACTIVE,
+  }).distinct('_id');
+  return activeNotificationUsers;
+};
 
-  /**
+/**
    * saved hook
    */
-  activitySchema.post('save', async(savedActivity: ActivityDocument) => {
-    const Notification = crowi.model('Notification');
-    try {
-      const notificationUsers = await savedActivity.getNotificationTargetUsers();
-
-      await Promise.all(notificationUsers.map(user => Notification.upsertByActivity(user, savedActivity)));
-      return;
-    }
-    catch (err) {
-      logger.error(err);
-    }
-  });
-
-  // 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) => {
-    const Notification = crowi.model('Notification');
-
-    try {
-      await Notification.removeActivity(activity);
-    }
-    catch (err) {
-      logger.error(err);
-    }
-  });
-
-  const Activity = model<ActivityDocument, ActivityModel>('Activity', activitySchema);
-
-  return Activity;
-};
+activitySchema.post('save', async(savedActivity: ActivityDocument) => {
+  try {
+    const notificationUsers = await savedActivity.getNotificationTargetUsers();
+
+    // await Promise.all(notificationUsers.map(user => InAppNotification.upsertByActivity(user, savedActivity)));
+    return;
+  }
+  catch (err) {
+    logger.error(err);
+  }
+});
+
+
+/**
+ * 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 };

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

@@ -116,7 +116,8 @@ module.exports = function(crowi) {
       throw err;
     }
 
-    await commentEvent.emit('update', savedComment.creator);
+
+    await commentEvent.emit('create', savedComment.creator);
   });
 
   return mongoose.model('Comment', commentSchema);

+ 104 - 108
packages/app/src/server/models/in-app-notification.ts

@@ -1,16 +1,14 @@
 import {
-  Types, Document, Model, Schema /* , Query */, model,
+  Types, Document, Model, Schema /* , Query */,
 } from 'mongoose';
 import { subDays } from 'date-fns';
 import ActivityDefine from '../util/activityDefine';
-import { getOrCreateModel } from '../util/mongoose-utils';
+import { getOrCreateModel, getModelSafely } from '../util/mongoose-utils';
 import loggerFactory from '../../utils/logger';
-import Crowi from '../crowi';
-import { ActivityDocument } from './activity';
-import User = require('./user');
-
+import { Activity, ActivityDocument } from '~/server/models/activity';
 
 const logger = loggerFactory('growi:models:inAppNotification');
+const User = getModelSafely('User') || require('~/server/models/user')();
 
 const STATUS_UNREAD = 'UNREAD';
 const STATUS_UNOPENED = 'UNOPENED';
@@ -43,108 +41,106 @@ export interface InAppNotificationModel extends Model<InAppNotificationDocument>
   STATUS_OPENED: string
 }
 
-export default (crowi: Crowi) => {
-  const inAppNotificationSchema = new Schema<InAppNotificationDocument, InAppNotificationModel>({
-    user: {
-      type: Schema.Types.ObjectId,
-      ref: 'User',
-      index: true,
-      require: true,
-    },
-    targetModel: {
-      type: String,
-      require: true,
-      enum: ActivityDefine.getSupportTargetModelNames(),
-    },
-    target: {
+const inAppNotificationSchema = new Schema<InAppNotificationDocument, InAppNotificationModel>({
+  user: {
+    type: Schema.Types.ObjectId,
+    ref: 'User',
+    index: true,
+    require: true,
+  },
+  targetModel: {
+    type: String,
+    require: true,
+    enum: ActivityDefine.getSupportTargetModelNames(),
+  },
+  target: {
+    type: Schema.Types.ObjectId,
+    refPath: 'targetModel',
+    require: true,
+  },
+  action: {
+    type: String,
+    require: true,
+    enum: ActivityDefine.getSupportActionNames(),
+  },
+  activities: [
+    {
       type: Schema.Types.ObjectId,
-      refPath: 'targetModel',
-      require: true,
-    },
-    action: {
-      type: String,
-      require: true,
-      enum: ActivityDefine.getSupportActionNames(),
-    },
-    activities: [
-      {
-        type: Schema.Types.ObjectId,
-        ref: 'Activity',
-      },
-    ],
-    status: {
-      type: String,
-      default: STATUS_UNREAD,
-      enum: STATUSES,
-      index: true,
-      require: true,
+      ref: 'Activity',
     },
-    createdAt: {
-      type: Date,
-      default: Date.now,
-    },
-  });
-  inAppNotificationSchema.virtual('actionUsers').get(function(this: InAppNotificationDocument) {
-    const Activity = crowi.model('Activity');
-    return Activity.getActionUsersFromActivities((this.activities as any) as ActivityDocument[]);
-  });
-  const transform = (doc, ret) => {
-    // delete ret.activities
-  };
-  inAppNotificationSchema.set('toObject', { virtuals: true, transform });
-  inAppNotificationSchema.set('toJSON', { virtuals: true, transform });
-  inAppNotificationSchema.index({
-    user: 1, target: 1, action: 1, createdAt: 1,
-  });
-
-  inAppNotificationSchema.statics.findLatestInAppNotificationsByUser = function(user, limitNum, offset) {
-    const limit = limitNum || 10;
-
-    return InAppNotification.find({ user })
-      .sort({ createdAt: -1 })
-      .skip(offset)
-      .limit(limit)
-      .populate(['user', 'target'])
-      .populate({ path: 'activities', populate: { path: 'user' } })
-      .exec();
-  };
-
-  inAppNotificationSchema.statics.removeEmpty = function() {
-    return InAppNotification.deleteMany({ activities: { $size: 0 } });
-  };
-
-  inAppNotificationSchema.statics.read = async function(user) {
-    const query = { user, status: STATUS_UNREAD };
-    const parameters = { status: STATUS_UNOPENED };
-
-    return InAppNotification.updateMany(query, parameters);
-  };
-
-  inAppNotificationSchema.statics.getUnreadCountByUser = async function(user) {
-    const query = { user, status: STATUS_UNREAD };
-
-    try {
-      const count = await InAppNotification.countDocuments(query);
-
-      return count;
-    }
-    catch (err) {
-      logger.error('Error on getUnreadCountByUser', err);
-      throw err;
-    }
-  };
-
-  inAppNotificationSchema.statics.STATUS_UNOPENED = function() {
-    return STATUS_UNOPENED;
-  };
-  inAppNotificationSchema.statics.STATUS_UNREAD = function() {
-    return STATUS_UNREAD;
-  };
-  inAppNotificationSchema.statics.STATUS_OPENED = function() {
-    return STATUS_OPENED;
-  };
-
-  const InAppNotification = getOrCreateModel<InAppNotificationDocument, InAppNotificationModel>('InAppNotification', inAppNotificationSchema);
-
-  return InAppNotification;
+  ],
+  status: {
+    type: String,
+    default: STATUS_UNREAD,
+    enum: STATUSES,
+    index: true,
+    require: true,
+  },
+  createdAt: {
+    type: Date,
+    default: Date.now,
+  },
+});
+inAppNotificationSchema.virtual('actionUsers').get(function(this: InAppNotificationDocument) {
+  return Activity.getActionUsersFromActivities((this.activities as any) as ActivityDocument[]);
+});
+const transform = (doc, ret) => {
+  // delete ret.activities
+};
+inAppNotificationSchema.set('toObject', { virtuals: true, transform });
+inAppNotificationSchema.set('toJSON', { virtuals: true, transform });
+inAppNotificationSchema.index({
+  user: 1, target: 1, action: 1, createdAt: 1,
+});
+
+inAppNotificationSchema.statics.findLatestInAppNotificationsByUser = function(user, limitNum, offset) {
+  const limit = limitNum || 10;
+
+  // TODO: improve populate refer to GROWI way by GW-7482
+  return InAppNotification.find({ user })
+    .sort({ createdAt: -1 })
+    .skip(offset)
+    .limit(limit)
+    .populate(['user', 'target'])
+    .populate({ path: 'activities', populate: { path: 'user' } })
+    .exec();
+};
+
+inAppNotificationSchema.statics.removeEmpty = function() {
+  return InAppNotification.deleteMany({ activities: { $size: 0 } });
 };
+
+inAppNotificationSchema.statics.read = async function(user) {
+  const query = { user, status: STATUS_UNREAD };
+  const parameters = { status: STATUS_UNOPENED };
+
+  return InAppNotification.updateMany(query, parameters);
+};
+
+inAppNotificationSchema.statics.getUnreadCountByUser = async function(user) {
+  const query = { user, status: STATUS_UNREAD };
+
+  try {
+    const count = await InAppNotification.countDocuments(query);
+
+    return count;
+  }
+  catch (err) {
+    logger.error('Error on getUnreadCountByUser', err);
+    throw err;
+  }
+};
+
+inAppNotificationSchema.statics.STATUS_UNOPENED = function() {
+  return STATUS_UNOPENED;
+};
+inAppNotificationSchema.statics.STATUS_UNREAD = function() {
+  return STATUS_UNREAD;
+};
+inAppNotificationSchema.statics.STATUS_OPENED = function() {
+  return STATUS_OPENED;
+};
+
+const InAppNotification = getOrCreateModel<InAppNotificationDocument, InAppNotificationModel>('InAppNotification', inAppNotificationSchema);
+
+export { InAppNotification };

+ 7 - 4
packages/app/src/server/service/in-app-notification.ts

@@ -1,5 +1,5 @@
 import Crowi from '../crowi';
-import InAppNotification from '~/server/models/in-app-notification';
+import { InAppNotification } from '~/server/models/in-app-notification';
 
 class InAppNotificationService {
 
@@ -16,10 +16,14 @@ class InAppNotificationService {
     this.commentEvent = crowi.event('comment');
 
     // init
-    this.updateCommentEvent();
+    this.initCommentEvent();
   }
 
-  updateCommentEvent(): void {
+  initCommentEvent(): void {
+    this.commentEvent.on('create', (user) => {
+      this.commentEvent.onCreate();
+    });
+
     this.commentEvent.on('update', (user) => {
       this.commentEvent.onUpdate();
 
@@ -31,7 +35,6 @@ class InAppNotificationService {
   }
 
   removeActivity = async function(activity) {
-    const InAppNotification = require('../models/in-app-notification')(this.crowi);
     const { _id, target, action } = activity;
     const query = { target, action };
     const parameters = { $pull: { activities: _id } };