mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
Auto-subscribe mentioned users to document (#12235)
* Auto-subscribe mentioned users to documnet * Add tests for mention auto-subscribe and a buildMention factory * Add tests that prior unsubscribes are respected when mentioned Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Batch mention subscriptions into a single transaction Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { QueryTypes } from "sequelize";
|
||||
import { QueryTypes, type Transaction } from "sequelize";
|
||||
import { SubscriptionType } from "@shared/types";
|
||||
import { createContext } from "@server/context";
|
||||
import type { Document } from "@server/models";
|
||||
import type { Document, User } from "@server/models";
|
||||
import { Subscription, Event } from "@server/models";
|
||||
import { sequelize } from "@server/storage/database";
|
||||
import type { APIContext, DocumentEvent, RevisionEvent } from "@server/types";
|
||||
@@ -105,34 +105,81 @@ export default async function subscriptionCreator({
|
||||
return subscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe a single user to a document. The subscription is created if it
|
||||
* does not exist; an existing subscription that has been deleted is left as-is
|
||||
* so that the user's prior unsubscribe is respected.
|
||||
*
|
||||
* @param user The user to subscribe.
|
||||
* @param document The document to subscribe the user to.
|
||||
* @param event The event that triggered the subscription creation.
|
||||
* @param options.transaction An existing transaction to run within. When
|
||||
* subscribing many users in a row, callers should open a single transaction
|
||||
* and pass it in to avoid the overhead of one BEGIN/COMMIT per call.
|
||||
*/
|
||||
export const subscribeUserToDocument = async (
|
||||
user: User,
|
||||
document: Document,
|
||||
event: DocumentEvent | RevisionEvent,
|
||||
options: { transaction?: Transaction } = {}
|
||||
): Promise<void> => {
|
||||
const run = (transaction: Transaction) =>
|
||||
subscriptionCreator({
|
||||
ctx: createContext({
|
||||
user,
|
||||
authType: event.authType,
|
||||
ip: event.ip,
|
||||
transaction,
|
||||
}),
|
||||
documentId: document.id,
|
||||
event: SubscriptionType.Document,
|
||||
resubscribe: false,
|
||||
});
|
||||
|
||||
if (options.transaction) {
|
||||
await run(options.transaction);
|
||||
return;
|
||||
}
|
||||
|
||||
await sequelize.transaction(run);
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribe a batch of users to a document inside a single transaction.
|
||||
*
|
||||
* @param users The users to subscribe.
|
||||
* @param document The document to subscribe the users to.
|
||||
* @param event The event that triggered the subscription creation.
|
||||
*/
|
||||
export const subscribeUsersToDocument = async (
|
||||
users: User[],
|
||||
document: Document,
|
||||
event: DocumentEvent | RevisionEvent
|
||||
): Promise<void> => {
|
||||
if (!users.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sequelize.transaction(async (transaction) => {
|
||||
for (const user of users) {
|
||||
await subscribeUserToDocument(user, document, event, { transaction });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create any new subscriptions that might be missing for collaborators in the
|
||||
* document on publish and revision creation. This does mean that there is a
|
||||
* short period of time where the user is not subscribed after editing until a
|
||||
* revision is created.
|
||||
*
|
||||
* @param document The document to create subscriptions for
|
||||
* @param event The event that triggered the subscription creation
|
||||
* @param document The document to create subscriptions for.
|
||||
* @param event The event that triggered the subscription creation.
|
||||
*/
|
||||
export const createSubscriptionsForDocument = async (
|
||||
document: Document,
|
||||
event: DocumentEvent | RevisionEvent
|
||||
): Promise<void> => {
|
||||
const users = await document.collaborators();
|
||||
|
||||
for (const user of users) {
|
||||
await sequelize.transaction(async (transaction) => {
|
||||
await subscriptionCreator({
|
||||
ctx: createContext({
|
||||
user,
|
||||
authType: event.authType,
|
||||
ip: event.ip,
|
||||
transaction,
|
||||
}),
|
||||
documentId: document.id,
|
||||
event: SubscriptionType.Document,
|
||||
resubscribe: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
await subscribeUsersToDocument(users, document, event);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { MentionType, NotificationEventType } from "@shared/types";
|
||||
import { Notification } from "@server/models";
|
||||
import {
|
||||
MentionType,
|
||||
NotificationEventType,
|
||||
SubscriptionType,
|
||||
} from "@shared/types";
|
||||
import { Notification, Subscription } from "@server/models";
|
||||
import {
|
||||
buildDocument,
|
||||
buildCollection,
|
||||
buildGroup,
|
||||
buildGroupUser,
|
||||
buildMention,
|
||||
buildProseMirrorDoc,
|
||||
buildUser,
|
||||
} from "@server/test/factories";
|
||||
import DocumentPublishedNotificationsTask from "./DocumentPublishedNotificationsTask";
|
||||
@@ -141,26 +146,19 @@ describe("documents.publish", () => {
|
||||
const document = await buildDocument({
|
||||
teamId: actor.teamId,
|
||||
userId: actor.id,
|
||||
content: {
|
||||
type: "doc",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
type: "mention",
|
||||
attrs: {
|
||||
id: uuidv4(),
|
||||
type: MentionType.Group,
|
||||
label: group.name,
|
||||
modelId: group.id,
|
||||
actorId: actor.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
content: buildProseMirrorDoc([
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
buildMention({
|
||||
type: MentionType.Group,
|
||||
modelId: group.id,
|
||||
actorId: actor.id,
|
||||
label: group.name,
|
||||
}),
|
||||
],
|
||||
},
|
||||
]).toJSON(),
|
||||
});
|
||||
|
||||
const processor = new DocumentPublishedNotificationsTask();
|
||||
@@ -175,4 +173,139 @@ describe("documents.publish", () => {
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should subscribe a mentioned user to the document", async () => {
|
||||
const actor = await buildUser();
|
||||
const mentioned = await buildUser({ teamId: actor.teamId });
|
||||
|
||||
const document = await buildDocument({
|
||||
teamId: actor.teamId,
|
||||
userId: actor.id,
|
||||
content: buildProseMirrorDoc([
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [buildMention({ modelId: mentioned.id, actorId: actor.id })],
|
||||
},
|
||||
]).toJSON(),
|
||||
});
|
||||
|
||||
const processor = new DocumentPublishedNotificationsTask();
|
||||
await processor.perform({
|
||||
name: "documents.publish",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId!,
|
||||
teamId: document.teamId,
|
||||
actorId: actor.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
const subscription = await Subscription.findOne({
|
||||
where: {
|
||||
userId: mentioned.id,
|
||||
documentId: document.id,
|
||||
event: SubscriptionType.Document,
|
||||
},
|
||||
});
|
||||
expect(subscription).not.toBeNull();
|
||||
});
|
||||
|
||||
it("should respect a prior unsubscribe when a user is mentioned", async () => {
|
||||
const actor = await buildUser();
|
||||
const mentioned = await buildUser({ teamId: actor.teamId });
|
||||
|
||||
const document = await buildDocument({
|
||||
teamId: actor.teamId,
|
||||
userId: actor.id,
|
||||
content: buildProseMirrorDoc([
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [buildMention({ modelId: mentioned.id, actorId: actor.id })],
|
||||
},
|
||||
]).toJSON(),
|
||||
});
|
||||
|
||||
// The mentioned user previously subscribed and then unsubscribed.
|
||||
const prior = await Subscription.create({
|
||||
userId: mentioned.id,
|
||||
documentId: document.id,
|
||||
event: SubscriptionType.Document,
|
||||
});
|
||||
await prior.destroy();
|
||||
|
||||
const processor = new DocumentPublishedNotificationsTask();
|
||||
await processor.perform({
|
||||
name: "documents.publish",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId!,
|
||||
teamId: document.teamId,
|
||||
actorId: actor.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
// No active subscription should exist.
|
||||
const active = await Subscription.findOne({
|
||||
where: {
|
||||
userId: mentioned.id,
|
||||
documentId: document.id,
|
||||
event: SubscriptionType.Document,
|
||||
},
|
||||
});
|
||||
expect(active).toBeNull();
|
||||
|
||||
// The original soft-deleted subscription should still be soft-deleted.
|
||||
const withDeleted = await Subscription.findOne({
|
||||
where: {
|
||||
userId: mentioned.id,
|
||||
documentId: document.id,
|
||||
event: SubscriptionType.Document,
|
||||
},
|
||||
paranoid: false,
|
||||
});
|
||||
expect(withDeleted).not.toBeNull();
|
||||
expect(withDeleted?.deletedAt).not.toBeNull();
|
||||
});
|
||||
|
||||
it("should not subscribe users mentioned via a group", async () => {
|
||||
const actor = await buildUser();
|
||||
const group = await buildGroup({ teamId: actor.teamId });
|
||||
const member = await buildUser({ teamId: actor.teamId });
|
||||
await buildGroupUser({ groupId: group.id, userId: member.id });
|
||||
|
||||
const document = await buildDocument({
|
||||
teamId: actor.teamId,
|
||||
userId: actor.id,
|
||||
content: buildProseMirrorDoc([
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
buildMention({
|
||||
type: MentionType.Group,
|
||||
modelId: group.id,
|
||||
actorId: actor.id,
|
||||
label: group.name,
|
||||
}),
|
||||
],
|
||||
},
|
||||
]).toJSON(),
|
||||
});
|
||||
|
||||
const processor = new DocumentPublishedNotificationsTask();
|
||||
await processor.perform({
|
||||
name: "documents.publish",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId!,
|
||||
teamId: document.teamId,
|
||||
actorId: actor.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
const subscription = await Subscription.findOne({
|
||||
where: {
|
||||
userId: member.id,
|
||||
documentId: document.id,
|
||||
event: SubscriptionType.Document,
|
||||
},
|
||||
});
|
||||
expect(subscription).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { MentionType, NotificationEventType } from "@shared/types";
|
||||
import { createSubscriptionsForDocument } from "@server/commands/subscriptionCreator";
|
||||
import {
|
||||
createSubscriptionsForDocument,
|
||||
subscribeUsersToDocument,
|
||||
} from "@server/commands/subscriptionCreator";
|
||||
import { Document, Group, Notification, User, GroupUser } from "@server/models";
|
||||
import { DocumentHelper } from "@server/models/helpers/DocumentHelper";
|
||||
import NotificationHelper from "@server/models/helpers/NotificationHelper";
|
||||
@@ -22,22 +25,32 @@ export default class DocumentPublishedNotificationsTask extends BaseTask<Documen
|
||||
const mentions = DocumentHelper.parseMentions(document, {
|
||||
type: MentionType.User,
|
||||
});
|
||||
const userIdsProcessed = new Set<string>();
|
||||
const userIdsMentioned: string[] = [];
|
||||
const usersToSubscribe: User[] = [];
|
||||
|
||||
for (const mention of mentions) {
|
||||
if (userIdsMentioned.includes(mention.modelId)) {
|
||||
if (userIdsProcessed.has(mention.modelId)) {
|
||||
continue;
|
||||
}
|
||||
userIdsProcessed.add(mention.modelId);
|
||||
|
||||
const recipient = await User.findByPk(mention.modelId);
|
||||
|
||||
if (
|
||||
recipient &&
|
||||
recipient.id !== mention.actorId &&
|
||||
!recipient ||
|
||||
recipient.id === mention.actorId ||
|
||||
!(await canUserAccessDocument(recipient, document.id))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
usersToSubscribe.push(recipient);
|
||||
|
||||
if (
|
||||
recipient.subscribedToEventType(
|
||||
NotificationEventType.MentionedInDocument
|
||||
) &&
|
||||
(await canUserAccessDocument(recipient, document.id))
|
||||
)
|
||||
) {
|
||||
await Notification.create({
|
||||
event: NotificationEventType.MentionedInDocument,
|
||||
@@ -50,6 +63,8 @@ export default class DocumentPublishedNotificationsTask extends BaseTask<Documen
|
||||
}
|
||||
}
|
||||
|
||||
await subscribeUsersToDocument(usersToSubscribe, document, event);
|
||||
|
||||
// send notifications to users in mentioned groups
|
||||
const groupMentions = DocumentHelper.parseMentions(document, {
|
||||
type: MentionType.Group,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { DeepPartial } from "utility-types";
|
||||
import type { ProsemirrorData } from "@shared/types";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { MentionType, NotificationEventType } from "@shared/types";
|
||||
import {
|
||||
MentionType,
|
||||
NotificationEventType,
|
||||
SubscriptionType,
|
||||
} from "@shared/types";
|
||||
import { createContext } from "@server/context";
|
||||
import { parser } from "@server/editor";
|
||||
import type { Document } from "@server/models";
|
||||
@@ -16,6 +17,8 @@ import {
|
||||
buildDocument,
|
||||
buildGroup,
|
||||
buildGroupUser,
|
||||
buildMention,
|
||||
buildProseMirrorDoc,
|
||||
buildUser,
|
||||
} from "@server/test/factories";
|
||||
import RevisionCreatedNotificationsTask from "./RevisionCreatedNotificationsTask";
|
||||
@@ -539,28 +542,20 @@ describe("revisions.create", () => {
|
||||
// Now add a mention – the only change is the mention node itself, which
|
||||
// renders as "@<label>" in plain text and may be below the 5-char
|
||||
// threshold that gates generic update notifications.
|
||||
const mentionContent: DeepPartial<ProsemirrorData> = {
|
||||
type: "doc",
|
||||
content: [
|
||||
...(document.content?.content ?? []),
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
type: "mention",
|
||||
attrs: {
|
||||
type: MentionType.User,
|
||||
label: mentioned.name,
|
||||
modelId: mentioned.id,
|
||||
actorId: actor.id,
|
||||
id: "test-mention-id",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
document.content = mentionContent as ProsemirrorData;
|
||||
document.content = buildProseMirrorDoc([
|
||||
...(document.content?.content ?? []),
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
buildMention({
|
||||
id: "test-mention-id",
|
||||
modelId: mentioned.id,
|
||||
actorId: actor.id,
|
||||
label: mentioned.name,
|
||||
}),
|
||||
],
|
||||
},
|
||||
]).toJSON();
|
||||
document.updatedAt = new Date();
|
||||
await document.save();
|
||||
|
||||
@@ -611,30 +606,23 @@ describe("revisions.create", () => {
|
||||
await Revision.createFromDocument(createContext({ user: actor }), document);
|
||||
|
||||
// Update document to include a group mention
|
||||
document.content = {
|
||||
type: "doc",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Updated content with a group mention ",
|
||||
},
|
||||
{
|
||||
type: "mention",
|
||||
attrs: {
|
||||
id: uuidv4(),
|
||||
type: MentionType.Group,
|
||||
label: group.name,
|
||||
modelId: group.id,
|
||||
actorId: actor.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
document.content = buildProseMirrorDoc([
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Updated content with a group mention ",
|
||||
},
|
||||
buildMention({
|
||||
type: MentionType.Group,
|
||||
modelId: group.id,
|
||||
actorId: actor.id,
|
||||
label: group.name,
|
||||
}),
|
||||
],
|
||||
},
|
||||
]).toJSON();
|
||||
document.updatedAt = new Date();
|
||||
await document.save();
|
||||
|
||||
@@ -655,4 +643,168 @@ describe("revisions.create", () => {
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should subscribe a mentioned user to the document", async () => {
|
||||
const actor = await buildUser();
|
||||
const mentioned = await buildUser({ teamId: actor.teamId, name: "Kim" });
|
||||
|
||||
const document = await buildDocument({
|
||||
teamId: actor.teamId,
|
||||
userId: actor.id,
|
||||
});
|
||||
await Revision.createFromDocument(createContext({ user: actor }), document);
|
||||
|
||||
document.content = buildProseMirrorDoc([
|
||||
...(document.content?.content ?? []),
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [buildMention({ modelId: mentioned.id, actorId: actor.id })],
|
||||
},
|
||||
]).toJSON();
|
||||
document.updatedAt = new Date();
|
||||
await document.save();
|
||||
|
||||
const revision = await Revision.createFromDocument(
|
||||
createContext({ user: actor }),
|
||||
document
|
||||
);
|
||||
|
||||
const task = new RevisionCreatedNotificationsTask();
|
||||
await task.perform({
|
||||
name: "revisions.create",
|
||||
documentId: document.id,
|
||||
teamId: document.teamId,
|
||||
actorId: actor.id,
|
||||
modelId: revision.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
const subscription = await Subscription.findOne({
|
||||
where: {
|
||||
userId: mentioned.id,
|
||||
documentId: document.id,
|
||||
event: SubscriptionType.Document,
|
||||
},
|
||||
});
|
||||
expect(subscription).not.toBeNull();
|
||||
});
|
||||
|
||||
it("should respect a prior unsubscribe when a user is mentioned", async () => {
|
||||
const actor = await buildUser();
|
||||
const mentioned = await buildUser({ teamId: actor.teamId });
|
||||
|
||||
const document = await buildDocument({
|
||||
teamId: actor.teamId,
|
||||
userId: actor.id,
|
||||
});
|
||||
await Revision.createFromDocument(createContext({ user: actor }), document);
|
||||
|
||||
// The mentioned user previously subscribed and then unsubscribed.
|
||||
const prior = await Subscription.create({
|
||||
userId: mentioned.id,
|
||||
documentId: document.id,
|
||||
event: SubscriptionType.Document,
|
||||
});
|
||||
await prior.destroy();
|
||||
|
||||
document.content = buildProseMirrorDoc([
|
||||
...(document.content?.content ?? []),
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [buildMention({ modelId: mentioned.id, actorId: actor.id })],
|
||||
},
|
||||
]).toJSON();
|
||||
document.updatedAt = new Date();
|
||||
await document.save();
|
||||
|
||||
const revision = await Revision.createFromDocument(
|
||||
createContext({ user: actor }),
|
||||
document
|
||||
);
|
||||
|
||||
const task = new RevisionCreatedNotificationsTask();
|
||||
await task.perform({
|
||||
name: "revisions.create",
|
||||
documentId: document.id,
|
||||
teamId: document.teamId,
|
||||
actorId: actor.id,
|
||||
modelId: revision.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
// No active subscription should exist.
|
||||
const active = await Subscription.findOne({
|
||||
where: {
|
||||
userId: mentioned.id,
|
||||
documentId: document.id,
|
||||
event: SubscriptionType.Document,
|
||||
},
|
||||
});
|
||||
expect(active).toBeNull();
|
||||
|
||||
// The original soft-deleted subscription should still be soft-deleted.
|
||||
const withDeleted = await Subscription.findOne({
|
||||
where: {
|
||||
userId: mentioned.id,
|
||||
documentId: document.id,
|
||||
event: SubscriptionType.Document,
|
||||
},
|
||||
paranoid: false,
|
||||
});
|
||||
expect(withDeleted).not.toBeNull();
|
||||
expect(withDeleted?.deletedAt).not.toBeNull();
|
||||
});
|
||||
|
||||
it("should not subscribe users mentioned via a group", async () => {
|
||||
const actor = await buildUser();
|
||||
const group = await buildGroup({ teamId: actor.teamId });
|
||||
const member = await buildUser({ teamId: actor.teamId });
|
||||
await buildGroupUser({ groupId: group.id, userId: member.id });
|
||||
|
||||
const document = await buildDocument({
|
||||
teamId: actor.teamId,
|
||||
userId: actor.id,
|
||||
});
|
||||
await Revision.createFromDocument(createContext({ user: actor }), document);
|
||||
|
||||
document.content = buildProseMirrorDoc([
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
buildMention({
|
||||
type: MentionType.Group,
|
||||
modelId: group.id,
|
||||
actorId: actor.id,
|
||||
label: group.name,
|
||||
}),
|
||||
],
|
||||
},
|
||||
]).toJSON();
|
||||
document.updatedAt = new Date();
|
||||
await document.save();
|
||||
|
||||
const revision = await Revision.createFromDocument(
|
||||
createContext({ user: actor }),
|
||||
document
|
||||
);
|
||||
|
||||
const task = new RevisionCreatedNotificationsTask();
|
||||
await task.perform({
|
||||
name: "revisions.create",
|
||||
documentId: document.id,
|
||||
teamId: document.teamId,
|
||||
actorId: actor.id,
|
||||
modelId: revision.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
const subscription = await Subscription.findOne({
|
||||
where: {
|
||||
userId: member.id,
|
||||
documentId: document.id,
|
||||
event: SubscriptionType.Document,
|
||||
},
|
||||
});
|
||||
expect(subscription).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,10 @@ import { subHours } from "date-fns";
|
||||
import { differenceBy } from "es-toolkit/compat";
|
||||
import { Op } from "sequelize";
|
||||
import { MentionType, NotificationEventType } from "@shared/types";
|
||||
import { createSubscriptionsForDocument } from "@server/commands/subscriptionCreator";
|
||||
import {
|
||||
createSubscriptionsForDocument,
|
||||
subscribeUsersToDocument,
|
||||
} from "@server/commands/subscriptionCreator";
|
||||
import env from "@server/env";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import {
|
||||
@@ -47,21 +50,31 @@ export default class RevisionCreatedNotificationsTask extends BaseTask<RevisionE
|
||||
];
|
||||
|
||||
const mentions = differenceBy(newMentions, oldMentions, "id");
|
||||
const userIdsProcessed = new Set<string>();
|
||||
const userIdsMentioned: string[] = [];
|
||||
const usersToSubscribe: User[] = [];
|
||||
for (const mention of mentions) {
|
||||
if (userIdsMentioned.includes(mention.modelId)) {
|
||||
if (userIdsProcessed.has(mention.modelId)) {
|
||||
continue;
|
||||
}
|
||||
userIdsProcessed.add(mention.modelId);
|
||||
|
||||
const recipient = await User.findByPk(mention.modelId);
|
||||
|
||||
if (
|
||||
recipient &&
|
||||
recipient.id !== mention.actorId &&
|
||||
!recipient ||
|
||||
recipient.id === mention.actorId ||
|
||||
!(await canUserAccessDocument(recipient, document.id))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
usersToSubscribe.push(recipient);
|
||||
|
||||
if (
|
||||
recipient.subscribedToEventType(
|
||||
NotificationEventType.MentionedInDocument
|
||||
) &&
|
||||
(await canUserAccessDocument(recipient, document.id))
|
||||
)
|
||||
) {
|
||||
await Notification.create({
|
||||
event: NotificationEventType.MentionedInDocument,
|
||||
@@ -76,6 +89,8 @@ export default class RevisionCreatedNotificationsTask extends BaseTask<RevisionE
|
||||
}
|
||||
}
|
||||
|
||||
await subscribeUsersToDocument(usersToSubscribe, document, event);
|
||||
|
||||
// Send notifications to users in mentioned groups
|
||||
const oldGroupMentions = before
|
||||
? DocumentHelper.parseMentions(before, { type: MentionType.Group })
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ImportState,
|
||||
IntegrationService,
|
||||
IntegrationType,
|
||||
MentionType,
|
||||
NotificationEventType,
|
||||
SubscriptionType,
|
||||
UserRole,
|
||||
@@ -910,6 +911,25 @@ export function buildProseMirrorDoc(content: DeepPartial<ProsemirrorData>[]) {
|
||||
});
|
||||
}
|
||||
|
||||
export function buildMention(overrides: {
|
||||
type?: MentionType;
|
||||
modelId: string;
|
||||
actorId: string;
|
||||
label?: string;
|
||||
id?: string;
|
||||
}): DeepPartial<ProsemirrorData> {
|
||||
return {
|
||||
type: "mention",
|
||||
attrs: {
|
||||
id: overrides.id ?? randomUUID(),
|
||||
type: overrides.type ?? MentionType.User,
|
||||
label: overrides.label ?? faker.name.fullName(),
|
||||
modelId: overrides.modelId,
|
||||
actorId: overrides.actorId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildCommentMark(overrides: {
|
||||
id?: string;
|
||||
userId?: string;
|
||||
|
||||
Reference in New Issue
Block a user