feat: Define actions that result in Create vs Edit activities#10818
feat: Define actions that result in Create vs Edit activities#10818mergify[bot] merged 31 commits intomasterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refines how page updates are recorded as activities by suppressing certain “edit/update” activity emissions based on recent activity history and revision count, aligning activity generation with the intended Create vs Edit behavior.
Changes:
- Add logic in the APIV3 page update route to look up the most recent CREATE/UPDATE activity for the page.
- Suppress emitting an UPDATE activity for rapid successive edits by the same user (within a 5-minute window), with additional gating based on revision count.
- Keep existing notification flows (global notification, Slack, AI vector store rebuild) while conditionally emitting the activity update event.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (shouldGenerateUpdateActivity) { | ||
| // persist activity | ||
| const creator = | ||
| updatedPage.creator != null | ||
| ? getIdForRef(updatedPage.creator) |
There was a problem hiding this comment.
ACTION_UNSETTLED のアクティビティがそのまま残るのであれば問題なさそう?
| const lastContentActivity = await Activity.findOne({ | ||
| target: targetPageId, | ||
| action: { | ||
| $in: [ | ||
| SupportedAction.ACTION_PAGE_CREATE, | ||
| SupportedAction.ACTION_PAGE_UPDATE, | ||
| ], | ||
| }, | ||
| _id: { $ne: res.locals.activity?._id }, | ||
| }).sort({ createdAt: -1 }); | ||
|
|
||
| // Check if last updated user is me | ||
| const isLastActivityByMe = | ||
| lastContentActivity && | ||
| lastContentActivity.user._id.toString() === req.user._id.toString(); | ||
|
|
||
| // Get time since activity | ||
| const lastActivityTime = lastContentActivity | ||
| ? lastContentActivity.createdAt.getTime() | ||
| : 0; | ||
| const timeSinceLastActivityMs = Date.now() - lastActivityTime; | ||
|
|
||
| // Decide if UPDATE activity should generate | ||
| let shouldGenerateUpdateActivity: boolean; | ||
| if (!isLastActivityByMe) { | ||
| shouldGenerateUpdateActivity = true; | ||
| } else if (timeSinceLastActivityMs < suppressUpdateWindow) { | ||
| shouldGenerateUpdateActivity = false; | ||
| } else { | ||
| const Revision = mongoose.model<IRevisionHasId>('Revision'); | ||
| const revisionCount = await Revision.countDocuments({ | ||
| pageId: updatedPage._id, | ||
| }); | ||
|
|
||
| shouldGenerateUpdateActivity = revisionCount > minimumRevisionForActivity; | ||
| } | ||
|
|
||
| if (shouldGenerateUpdateActivity) { | ||
| // persist activity | ||
| const creator = | ||
| updatedPage.creator != null | ||
| ? getIdForRef(updatedPage.creator) | ||
| : undefined; | ||
| const parameters = { | ||
| targetModel: SupportedTargetModel.MODEL_PAGE, | ||
| target: updatedPage, | ||
| action: SupportedAction.ACTION_PAGE_UPDATE, | ||
| }; | ||
| const activityEvent = crowi.events.activity; | ||
| activityEvent.emit( | ||
| 'update', | ||
| res.locals.activity._id, | ||
| parameters, | ||
| { path: updatedPage.path, creator }, | ||
| preNotifyService.generatePreNotify, | ||
| ); | ||
| } |
| lastContentActivity && | ||
| lastContentActivity.user._id.toString() === req.user._id.toString(); |
There was a problem hiding this comment.
前編集したユーザーが削除された場合はlastContentActivity.user がnullになるかもしれないので対応しました
| const minimumRevisionForActivity = 2; | ||
| const suppressUpdateWindow = 5 * 60 * 1000; // 5 min |
| const lastContentActivity = await Activity.findOne({ | ||
| target: targetPageId, | ||
| action: { | ||
| $in: [ | ||
| SupportedAction.ACTION_PAGE_CREATE, | ||
| SupportedAction.ACTION_PAGE_UPDATE, | ||
| ], | ||
| }, | ||
| _id: { $ne: res.locals.activity?._id }, | ||
| }).sort({ createdAt: -1 }); | ||
|
|
||
| // Check if last updated user is me | ||
| const isLastActivityByMe = | ||
| lastContentActivity && | ||
| lastContentActivity.user._id.toString() === req.user._id.toString(); | ||
|
|
||
| // Get time since activity | ||
| const lastActivityTime = lastContentActivity | ||
| ? lastContentActivity.createdAt.getTime() | ||
| : 0; | ||
| const timeSinceLastActivityMs = Date.now() - lastActivityTime; | ||
|
|
||
| // Decide if UPDATE activity should generate | ||
| let shouldGenerateUpdateActivity: boolean; | ||
| if (!isLastActivityByMe) { | ||
| shouldGenerateUpdateActivity = true; | ||
| } else if (timeSinceLastActivityMs < suppressUpdateWindow) { | ||
| shouldGenerateUpdateActivity = false; | ||
| } else { | ||
| const Revision = mongoose.model<IRevisionHasId>('Revision'); | ||
| const revisionCount = await Revision.countDocuments({ | ||
| pageId: updatedPage._id, | ||
| }); | ||
|
|
||
| shouldGenerateUpdateActivity = revisionCount > minimumRevisionForActivity; | ||
| } | ||
|
|
||
| if (shouldGenerateUpdateActivity) { | ||
| // persist activity | ||
| const creator = | ||
| updatedPage.creator != null | ||
| ? getIdForRef(updatedPage.creator) | ||
| : undefined; | ||
| const parameters = { | ||
| targetModel: SupportedTargetModel.MODEL_PAGE, | ||
| target: updatedPage, | ||
| action: SupportedAction.ACTION_PAGE_UPDATE, | ||
| }; | ||
| const activityEvent = crowi.events.activity; | ||
| activityEvent.emit( | ||
| 'update', | ||
| res.locals.activity._id, | ||
| parameters, | ||
| { path: updatedPage.path, creator }, | ||
| preNotifyService.generatePreNotify, | ||
| ); | ||
| } |
|
|
||
| // Check if last updated user is me | ||
| const isLastActivityByMe = | ||
| lastContentActivity && |
| lastContentActivity && | ||
| lastContentActivity.user._id.toString() === req.user._id.toString(); |
| if (shouldGenerateUpdateActivity) { | ||
| // persist activity | ||
| const creator = | ||
| updatedPage.creator != null | ||
| ? getIdForRef(updatedPage.creator) |
There was a problem hiding this comment.
ACTION_UNSETTLED のアクティビティがそのまま残るのであれば問題なさそう?
| { path: updatedPage.path, creator }, | ||
| preNotifyService.generatePreNotify, | ||
| ); | ||
| const minimumRevisionForActivity = 2; |
There was a problem hiding this comment.
2が正しいでした:
- Revision 1: ページ作成
- Revision 2: 初めての編集 (activityが発生しない)
- Revision 3: 2つ目の編集 (activityが発生する)
| const minimumRevisionForActivity = 2; | ||
| const suppressUpdateWindowMs = 5 * 60 * 1000; // 5 min |
| lastContentActivity?.user?._id?.toString() === | ||
| req.user?._id?.toString(); | ||
|
|
||
| const lastActivityTime = lastContentActivity |
| { path: updatedPage.path, creator }, | ||
| preNotifyService.generatePreNotify, | ||
| ); | ||
| const minimumRevisionForActivity = 2; |
…ho is not creator
| try { | ||
| const targetPageId = updatedPage._id.toString(); | ||
| const currentActivityId = res.locals.activity?._id.toString(); | ||
| const currentUserId = req.user?._id?.toString(); |
There was a problem hiding this comment.
currentUserId が undefined の場合、isLastActivityByMe は常に false となり、常にアクティビティが生成されてしまう。認証済みルートなので実際には起きにくいが、型定義を string | undefined に明示し、undefined時の振る舞いを意図的にすべき。
|
|
||
| const isLastActivityByMe = | ||
| !!currentUserId && | ||
| lastContentActivity?.user?._id?.toString() === currentUserId; |
There was a problem hiding this comment.
import { getIdForRef, getIdStringForRef } from '@growi/core';
を利用してください
Merge Queue Status
This pull request spent 11 minutes 27 seconds in the queue, including 10 minutes 57 seconds running CI. Required conditions to merge
|
https://redmine.weseek.co.jp/issues/177734
https://redmine.weseek.co.jp/issues/177877
Rules for Update Activity