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

Merge pull request #4256 from weseek/imprv/gw7221-getIo-method

Imprv/gw7221 get io method
Yuki Takei 4 лет назад
Родитель
Сommit
1e36f47b27

+ 1 - 1
packages/app/src/components/PageEditor/InAppNotificationDropdown.tsx

@@ -85,7 +85,7 @@ export const InAppNotificationDropdown: FC<Props> = (props: Props) => {
   };
   };
 
 
   /**
   /**
-    * TODO: Jump to the page by Click the notification by GW-7472
+    * TODO: Jump to the page by clicking on the notification by GW-7472
     */
     */
 
 
   const handleNotificationOnClick = async(notification: Notification) => {
   const handleNotificationOnClick = async(notification: Notification) => {

+ 11 - 0
packages/app/src/server/crowi/index.js

@@ -63,6 +63,7 @@ function Crowi() {
   this.cdnResourcesService = new CdnResourcesService();
   this.cdnResourcesService = new CdnResourcesService();
   this.interceptorManager = new InterceptorManager();
   this.interceptorManager = new InterceptorManager();
   this.slackIntegrationService = null;
   this.slackIntegrationService = null;
+  this.inAppNotificationService = null;
   this.xss = new Xss();
   this.xss = new Xss();
 
 
   this.tokens = null;
   this.tokens = null;
@@ -80,6 +81,7 @@ function Crowi() {
     bookmark: new (require('../events/bookmark'))(this),
     bookmark: new (require('../events/bookmark'))(this),
     tag: new (require('../events/tag'))(this),
     tag: new (require('../events/tag'))(this),
     admin: new (require('../events/admin'))(this),
     admin: new (require('../events/admin'))(this),
+    comment: new (require('../events/comment'))(this),
   };
   };
 }
 }
 
 
@@ -119,6 +121,7 @@ Crowi.prototype.init = async function() {
     this.setupExport(),
     this.setupExport(),
     this.setupImport(),
     this.setupImport(),
     this.setupPageService(),
     this.setupPageService(),
+    this.setupInAppNotificationService(),
     this.setupSyncPageStatusService(),
     this.setupSyncPageStatusService(),
   ]);
   ]);
 
 
@@ -157,6 +160,7 @@ Crowi.prototype.initForTest = async function() {
     // this.setupExport(),
     // this.setupExport(),
     // this.setupImport(),
     // this.setupImport(),
     this.setupPageService(),
     this.setupPageService(),
+    this.setupInAppNotificationService(),
   ]);
   ]);
 
 
   // globalNotification depends on slack and mailer
   // globalNotification depends on slack and mailer
@@ -635,6 +639,13 @@ Crowi.prototype.setupPageService = async function() {
   }
   }
 };
 };
 
 
+Crowi.prototype.setupInAppNotificationService = async function() {
+  const InAppNotificationService = require('../service/in-app-notification');
+  if (this.inAppNotificationService == null) {
+    this.inAppNotificationService = new InAppNotificationService(this);
+  }
+};
+
 Crowi.prototype.setupSyncPageStatusService = async function() {
 Crowi.prototype.setupSyncPageStatusService = async function() {
   const SyncPageStatusService = require('../service/system-events/sync-page-status');
   const SyncPageStatusService = require('../service/system-events/sync-page-status');
   if (this.syncPageStatusService == null) {
   if (this.syncPageStatusService == null) {

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

@@ -0,0 +1,26 @@
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:events:comment');
+
+const util = require('util');
+const events = require('events');
+
+function CommentEvent(crowi) {
+  this.crowi = crowi;
+
+  events.EventEmitter.call(this);
+}
+
+util.inherits(CommentEvent, events.EventEmitter);
+
+
+CommentEvent.prototype.onCreate = function() {
+  logger.info('onCreate comment event fired');
+};
+
+CommentEvent.prototype.onUpdate = function() {
+  logger.info('onUpdate comment event fired');
+};
+
+
+module.exports = CommentEvent;

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

@@ -1,11 +1,15 @@
 import { DeleteWriteOpResultObject } from 'mongodb';
 import { DeleteWriteOpResultObject } from 'mongodb';
 import {
 import {
-  Types, Document, Model, Schema, model,
+  Types, Document, Model, Schema,
 } from 'mongoose';
 } 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 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');
 const logger = loggerFactory('growi:models:activity');
 
 
@@ -33,196 +37,195 @@ export interface ActivityModel extends Model<ActivityDocument> {
   getActionUsersFromActivities(activities: ActivityDocument[]): any[]
   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
    * @param {object} parameters
    * @return {Promise}
    * @return {Promise}
    */
    */
-  activitySchema.statics.createByParameters = function(parameters) {
-    return Activity.create(parameters);
-  };
+activitySchema.statics.createByParameters = function(parameters) {
+  return this.create(parameters);
+};
 
 
-  /**
+/**
    * @param {object} 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
    * @param {Comment} comment
    * @return {Promise}
    * @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 {Page} page
    * @param {User} user
    * @param {User} user
    * @return {Promise}
    * @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 {Page} page
    * @param {User} user
    * @param {User} user
    * @return {Promise}
    * @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
    * @param {Page} page
    * @return {Promise}
    * @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
    * @param {User} user
    * @return {Promise}
    * @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
    * 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 };

+ 18 - 9
packages/app/src/server/models/comment.js

@@ -65,14 +65,18 @@ module.exports = function(crowi) {
     }));
     }));
   };
   };
 
 
-  commentSchema.statics.updateCommentsByPageId = function(comment, isMarkdown, commentId) {
+  commentSchema.statics.updateCommentsByPageId = async function(comment, isMarkdown, commentId) {
     const Comment = this;
     const Comment = this;
+    const commentEvent = crowi.event('comment');
 
 
-    return Comment.findOneAndUpdate(
+    const commentData = await Comment.findOneAndUpdate(
       { _id: commentId },
       { _id: commentId },
       { $set: { comment, isMarkdown } },
       { $set: { comment, isMarkdown } },
     );
     );
 
 
+    await commentEvent.emit('update', commentData.creator);
+
+    return commentData;
   };
   };
 
 
   commentSchema.statics.removeCommentsByPageId = function(pageId) {
   commentSchema.statics.removeCommentsByPageId = function(pageId) {
@@ -100,15 +104,20 @@ module.exports = function(crowi) {
   /**
   /**
    * post save hook
    * post save hook
    */
    */
-  commentSchema.post('save', (savedComment) => {
+  commentSchema.post('save', async(savedComment) => {
     const Page = crowi.model('Page');
     const Page = crowi.model('Page');
+    const commentEvent = crowi.event('comment');
 
 
-    Page.updateCommentCount(savedComment.page)
-      .then((page) => {
-        debug('CommentCount Updated', page);
-      })
-      .catch(() => {
-      });
+    try {
+      const page = await Page.updateCommentCount(savedComment.page);
+      debug('CommentCount Updated', page);
+    }
+    catch (err) {
+      throw err;
+    }
+
+
+    await commentEvent.emit('create', savedComment.creator);
   });
   });
 
 
   return mongoose.model('Comment', commentSchema);
   return mongoose.model('Comment', commentSchema);

+ 146 - 0
packages/app/src/server/models/in-app-notification.ts

@@ -0,0 +1,146 @@
+import {
+  Types, Document, Model, Schema /* , Query */,
+} from 'mongoose';
+import { subDays } from 'date-fns';
+import ActivityDefine from '../util/activityDefine';
+import { getOrCreateModel, getModelSafely } from '../util/mongoose-utils';
+import loggerFactory from '../../utils/logger';
+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';
+const STATUS_OPENED = 'OPENED';
+const STATUSES = [STATUS_UNREAD, STATUS_UNOPENED, STATUS_OPENED];
+
+export interface InAppNotificationDocument extends Document {
+  _id: Types.ObjectId
+  user: Types.ObjectId
+  targetModel: string
+  target: Types.ObjectId
+  action: string
+  activities: Types.ObjectId[]
+  status: string
+  createdAt: Date
+}
+
+export interface InAppNotificationModel extends Model<InAppNotificationDocument> {
+  findLatestInAppNotificationsByUser(user: Types.ObjectId, skip: number, offset: number): Promise<InAppNotificationDocument[]>
+  upsertByActivity(user: 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>> */
+
+  open(user: typeof User, id: Types.ObjectId): Promise<InAppNotificationDocument | null>
+  getUnreadCountByUser(user: Types.ObjectId): Promise<number | undefined>
+
+  STATUS_UNREAD: string
+  STATUS_UNOPENED: string
+  STATUS_OPENED: string
+}
+
+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,
+      ref: 'Activity',
+    },
+  ],
+  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 };

+ 0 - 218
packages/app/src/server/models/notification.ts

@@ -1,218 +0,0 @@
-import {
-  Types, Document, Model, Schema /* , Query */, model,
-} from 'mongoose';
-import { subDays } from 'date-fns';
-import ActivityDefine from '../util/activityDefine';
-import loggerFactory from '~/utils/logger';
-import Crowi from '../crowi';
-import { ActivityDocument } from './activity';
-import User = require('./user');
-
-const logger = loggerFactory('growi:models:notification');
-
-const STATUS_UNREAD = 'UNREAD';
-const STATUS_UNOPENED = 'UNOPENED';
-const STATUS_OPENED = 'OPENED';
-const STATUSES = [STATUS_UNREAD, STATUS_UNOPENED, STATUS_OPENED];
-
-export interface NotificationDocument extends Document {
-  _id: Types.ObjectId
-  user: Types.ObjectId
-  targetModel: string
-  target: Types.ObjectId
-  action: string
-  activities: Types.ObjectId[]
-  status: string
-  createdAt: Date
-}
-
-export interface NotificationModel extends Model<NotificationDocument> {
-  findLatestNotificationsByUser(user: Types.ObjectId, skip: number, offset: number): Promise<NotificationDocument[]>
-  upsertByActivity(user: Types.ObjectId, activity: ActivityDocument, createdAt?: Date | null): Promise<NotificationDocument | null>
-  removeActivity(activity: any): any
-  // commented out type 'Query' temporary to avoid ts error
-  removeEmpty()/* : Query<any> */
-  read(user: typeof User) /* : Promise<Query<any>> */
-
-  open(user: typeof User, id: Types.ObjectId): Promise<NotificationDocument | null>
-  getUnreadCountByUser(user: Types.ObjectId): Promise<number | undefined>
-
-  STATUS_UNREAD: string
-  STATUS_UNOPENED: string
-  STATUS_OPENED: string
-}
-
-export default (crowi: Crowi) => {
-  const notificationEvent = crowi.event('Notification');
-
-  const notificationSchema = new Schema<NotificationDocument, NotificationModel>({
-    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,
-        ref: 'Activity',
-      },
-    ],
-    status: {
-      type: String,
-      default: STATUS_UNREAD,
-      enum: STATUSES,
-      index: true,
-      require: true,
-    },
-    createdAt: {
-      type: Date,
-      default: Date.now,
-    },
-  });
-  notificationSchema.virtual('actionUsers').get(function(this: NotificationDocument) {
-    const Activity = crowi.model('Activity');
-    return Activity.getActionUsersFromActivities((this.activities as any) as ActivityDocument[]);
-  });
-  const transform = (doc, ret) => {
-    // delete ret.activities
-  };
-  notificationSchema.set('toObject', { virtuals: true, transform });
-  notificationSchema.set('toJSON', { virtuals: true, transform });
-  notificationSchema.index({
-    user: 1, target: 1, action: 1, createdAt: 1,
-  });
-
-  notificationSchema.statics.findLatestNotificationsByUser = function(user, limitNum, offset) {
-    const limit = limitNum || 10;
-
-    return Notification.find({ user })
-      .sort({ createdAt: -1 })
-      .skip(offset)
-      .limit(limit)
-      .populate(['user', 'target'])
-      .populate({ path: 'activities', populate: { path: 'user' } })
-      .exec();
-  };
-
-  notificationSchema.statics.upsertByActivity = async function(user, activity, createdAt = null) {
-    const {
-      _id: activityId, targetModel, target, action,
-    } = activity;
-
-    const now = createdAt || Date.now();
-    const lastWeek = subDays(now, 7);
-    const query = {
-      user, target, action, createdAt: { $gt: lastWeek },
-    };
-    const parameters = {
-      user,
-      targetModel,
-      target,
-      action,
-      status: STATUS_UNREAD,
-      createdAt: now,
-      $addToSet: { activities: activityId },
-    };
-
-    const options = {
-      upsert: true,
-      new: true,
-      setDefaultsOnInsert: true,
-      runValidators: true,
-    };
-
-    const notification = await Notification.findOneAndUpdate(query, parameters, options);
-
-    if (notification) {
-      notificationEvent.emit('update', notification.user);
-    }
-
-    return notification;
-  };
-
-  notificationSchema.statics.removeActivity = async function(activity) {
-    const { _id, target, action } = activity;
-    const query = { target, action };
-    const parameters = { $pull: { activities: _id } };
-
-    const result = await Notification.updateMany(query, parameters);
-
-    await Notification.removeEmpty();
-    return result;
-  };
-
-  notificationSchema.statics.removeEmpty = function() {
-    return Notification.deleteMany({ activities: { $size: 0 } });
-  };
-
-  notificationSchema.statics.read = async function(user) {
-    const query = { user, status: STATUS_UNREAD };
-    const parameters = { status: STATUS_UNOPENED };
-
-    return Notification.updateMany(query, parameters);
-  };
-
-  notificationSchema.statics.open = async function(user, id) {
-    const query = { _id: id, user: user._id };
-    const parameters = { status: STATUS_OPENED };
-    const options = { new: true };
-
-    const notification = await Notification.findOneAndUpdate(query, parameters, options);
-    if (notification) {
-      notificationEvent.emit('update', notification.user);
-    }
-    return notification;
-  };
-
-  notificationSchema.statics.getUnreadCountByUser = async function(user) {
-    const query = { user, status: STATUS_UNREAD };
-
-    try {
-      const count = await Notification.countDocuments(query);
-
-      return count;
-    }
-    catch (err) {
-      logger.error('Error on getUnreadCountByUser', err);
-      throw err;
-    }
-  };
-
-  notificationEvent.on('update', (user) => {
-    // TODO: be able to use getIo method by GW7221
-    // const io = crowi.getIo();
-    // if (io) {
-    //   io.sockets.emit('notification updated', { user });
-    // }
-  });
-
-  notificationSchema.statics.STATUS_UNOPENED = function() {
-    return STATUS_UNOPENED;
-  };
-  notificationSchema.statics.STATUS_UNREAD = function() {
-    return STATUS_UNREAD;
-  };
-  notificationSchema.statics.STATUS_OPENED = function() {
-    return STATUS_OPENED;
-  };
-
-  const Notification = model<NotificationDocument, NotificationModel>('Notification', notificationSchema);
-
-  return Notification;
-};

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

@@ -0,0 +1,46 @@
+import Crowi from '../crowi';
+import { InAppNotification } from '~/server/models/in-app-notification';
+
+class InAppNotificationService {
+
+  crowi!: any;
+
+  socketIoService!: any;
+
+  commentEvent!: any;
+
+
+  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();
+    });
+  }
+
+  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;