chore(server): send comment notification to all repliers (#13063)
close AF-2714 #### PR Dependency Tree * **PR #13063** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added the ability to list all replies for a specific comment. * **Bug Fixes** * Improved notification delivery for comment replies, ensuring all relevant users (comment author, document owner, and all repliers) are notified appropriately. * **Tests** * Added and updated tests to verify correct notification behavior and the new reply listing functionality. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -688,7 +688,7 @@ e2e(
|
|||||||
);
|
);
|
||||||
|
|
||||||
e2e(
|
e2e(
|
||||||
'should create reply and send comment mention notification to comment author',
|
'should create reply and send comment notification to comment author',
|
||||||
async t => {
|
async t => {
|
||||||
const docId = randomUUID();
|
const docId = randomUUID();
|
||||||
await app.create(Mockers.DocUser, {
|
await app.create(Mockers.DocUser, {
|
||||||
@@ -740,12 +740,12 @@ e2e(
|
|||||||
t.is(notification.name, 'notification.sendComment');
|
t.is(notification.name, 'notification.sendComment');
|
||||||
t.is(notification.payload.userId, member.id);
|
t.is(notification.payload.userId, member.id);
|
||||||
t.is(notification.payload.body.replyId, result.createReply.id);
|
t.is(notification.payload.body.replyId, result.createReply.id);
|
||||||
t.is(notification.payload.isMention, true);
|
t.is(notification.payload.isMention, undefined);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
e2e(
|
e2e(
|
||||||
'should create reply and send comment mention notification to comment author only when author is doc owner',
|
'should create reply and send comment notification to comment author only when author is doc owner',
|
||||||
async t => {
|
async t => {
|
||||||
const docId = randomUUID();
|
const docId = randomUUID();
|
||||||
await app.create(Mockers.DocUser, {
|
await app.create(Mockers.DocUser, {
|
||||||
@@ -789,6 +789,61 @@ e2e(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
t.truthy(result.createReply.id);
|
||||||
|
t.is(result.createReply.commentId, createResult.createComment.id);
|
||||||
|
t.is(app.queue.count('notification.sendComment'), count + 1);
|
||||||
|
const notification = app.queue.last('notification.sendComment');
|
||||||
|
t.is(notification.name, 'notification.sendComment');
|
||||||
|
t.is(notification.payload.userId, member.id);
|
||||||
|
t.is(notification.payload.body.replyId, result.createReply.id);
|
||||||
|
t.is(notification.payload.isMention, undefined);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
e2e('should send comment mention notification is high priority', async t => {
|
||||||
|
const docId = randomUUID();
|
||||||
|
await app.create(Mockers.DocUser, {
|
||||||
|
workspaceId: teamWorkspace.id,
|
||||||
|
docId,
|
||||||
|
userId: member.id,
|
||||||
|
type: DocRole.Owner,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.login(member);
|
||||||
|
const createResult = await app.gql({
|
||||||
|
query: createCommentMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
workspaceId: teamWorkspace.id,
|
||||||
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
|
content: {
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: 'test' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.login(owner);
|
||||||
|
const count = app.queue.count('notification.sendComment');
|
||||||
|
const result = await app.gql({
|
||||||
|
query: createReplyMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
commentId: createResult.createComment.id,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
|
content: {
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: 'test' }],
|
||||||
|
},
|
||||||
|
mentions: [member.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
t.truthy(result.createReply.id);
|
t.truthy(result.createReply.id);
|
||||||
t.is(result.createReply.commentId, createResult.createComment.id);
|
t.is(result.createReply.commentId, createResult.createComment.id);
|
||||||
t.is(app.queue.count('notification.sendComment'), count + 1);
|
t.is(app.queue.count('notification.sendComment'), count + 1);
|
||||||
@@ -797,6 +852,84 @@ e2e(
|
|||||||
t.is(notification.payload.userId, member.id);
|
t.is(notification.payload.userId, member.id);
|
||||||
t.is(notification.payload.body.replyId, result.createReply.id);
|
t.is(notification.payload.body.replyId, result.createReply.id);
|
||||||
t.is(notification.payload.isMention, true);
|
t.is(notification.payload.isMention, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e(
|
||||||
|
'should create reply and send comment notification to all repliers',
|
||||||
|
async t => {
|
||||||
|
const docId = randomUUID();
|
||||||
|
await app.create(Mockers.DocUser, {
|
||||||
|
workspaceId: teamWorkspace.id,
|
||||||
|
docId,
|
||||||
|
userId: member.id,
|
||||||
|
type: DocRole.Owner,
|
||||||
|
});
|
||||||
|
await app.create(Mockers.DocUser, {
|
||||||
|
workspaceId: teamWorkspace.id,
|
||||||
|
docId,
|
||||||
|
userId: other.id,
|
||||||
|
type: DocRole.Commenter,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.login(member);
|
||||||
|
const createResult = await app.gql({
|
||||||
|
query: createCommentMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
workspaceId: teamWorkspace.id,
|
||||||
|
docId,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
|
content: {
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: 'test' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.login(owner);
|
||||||
|
await app.gql({
|
||||||
|
query: createReplyMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
commentId: createResult.createComment.id,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
|
content: {
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: 'test' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// notify to all repliers: member and owner
|
||||||
|
const count = app.queue.count('notification.sendComment');
|
||||||
|
await app.login(other);
|
||||||
|
const result = await app.gql({
|
||||||
|
query: createReplyMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
commentId: createResult.createComment.id,
|
||||||
|
docMode: DocMode.page,
|
||||||
|
docTitle: 'test',
|
||||||
|
content: {
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: 'test' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
t.truthy(result.createReply.id);
|
||||||
|
t.is(result.createReply.commentId, createResult.createComment.id);
|
||||||
|
t.is(app.queue.count('notification.sendComment'), count + 2);
|
||||||
|
const notification = app.queue.last('notification.sendComment');
|
||||||
|
t.is(notification.name, 'notification.sendComment');
|
||||||
|
t.is(notification.payload.userId, owner.id);
|
||||||
|
t.is(notification.payload.body.replyId, result.createReply.id);
|
||||||
|
t.is(notification.payload.isMention, undefined);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -375,10 +375,7 @@ export class CommentResolver {
|
|||||||
reply?: Reply
|
reply?: Reply
|
||||||
) {
|
) {
|
||||||
const mentionUserIds = new Set(mentions);
|
const mentionUserIds = new Set(mentions);
|
||||||
// send comment mention notification to comment author on reply
|
const notifyUserIds = new Set<string>();
|
||||||
if (reply) {
|
|
||||||
mentionUserIds.add(comment.userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// send comment mention notification to mentioned users
|
// send comment mention notification to mentioned users
|
||||||
for (const mentionUserId of mentionUserIds) {
|
for (const mentionUserId of mentionUserIds) {
|
||||||
@@ -420,14 +417,28 @@ export class CommentResolver {
|
|||||||
comment.workspaceId,
|
comment.workspaceId,
|
||||||
comment.docId
|
comment.docId
|
||||||
);
|
);
|
||||||
// if the owner is not in the mention user ids, send comment notification to the owner
|
if (owner) {
|
||||||
if (
|
notifyUserIds.add(owner.userId);
|
||||||
owner &&
|
}
|
||||||
owner.userId !== sender.id &&
|
|
||||||
!mentionUserIds.has(owner.userId)
|
// send comment notification to all repliers and comment author
|
||||||
) {
|
if (reply) {
|
||||||
|
notifyUserIds.add(comment.userId);
|
||||||
|
const replies = await this.models.comment.listReplies(
|
||||||
|
comment.workspaceId,
|
||||||
|
comment.docId,
|
||||||
|
comment.id
|
||||||
|
);
|
||||||
|
for (const reply of replies) {
|
||||||
|
notifyUserIds.add(reply.userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const userId of notifyUserIds) {
|
||||||
|
// skip if the user is the sender or mentioned
|
||||||
|
if (userId !== sender.id && !mentionUserIds.has(userId)) {
|
||||||
await this.queue.add('notification.sendComment', {
|
await this.queue.add('notification.sendComment', {
|
||||||
userId: owner.userId,
|
userId,
|
||||||
body: {
|
body: {
|
||||||
workspaceId: comment.workspaceId,
|
workspaceId: comment.workspaceId,
|
||||||
createdByUserId: sender.id,
|
createdByUserId: sender.id,
|
||||||
@@ -442,6 +453,7 @@ export class CommentResolver {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async assertPermission(
|
private async assertPermission(
|
||||||
me: UserType,
|
me: UserType,
|
||||||
|
|||||||
@@ -524,3 +524,43 @@ test('should list changes', async t => {
|
|||||||
});
|
});
|
||||||
t.is(changes5.length, 0);
|
t.is(changes5.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should list replies', async t => {
|
||||||
|
const docId = randomUUID();
|
||||||
|
const comment = await models.comment.create({
|
||||||
|
content: {
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: 'test' }],
|
||||||
|
},
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
docId,
|
||||||
|
userId: owner.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const reply1 = await models.comment.createReply({
|
||||||
|
userId: owner.id,
|
||||||
|
content: {
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: 'test reply1' }],
|
||||||
|
},
|
||||||
|
commentId: comment.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const reply2 = await models.comment.createReply({
|
||||||
|
userId: owner.id,
|
||||||
|
content: {
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{ type: 'text', text: 'test reply2' }],
|
||||||
|
},
|
||||||
|
commentId: comment.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const replies = await models.comment.listReplies(
|
||||||
|
workspace.id,
|
||||||
|
docId,
|
||||||
|
comment.id
|
||||||
|
);
|
||||||
|
t.is(replies.length, 2);
|
||||||
|
t.is(replies[0].id, reply1.id);
|
||||||
|
t.is(replies[1].id, reply2.id);
|
||||||
|
});
|
||||||
|
|||||||
@@ -300,6 +300,13 @@ export class CommentModel extends BaseModel {
|
|||||||
})) as Reply | null;
|
})) as Reply | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listReplies(workspaceId: string, docId: string, commentId: string) {
|
||||||
|
return (await this.db.reply.findMany({
|
||||||
|
where: { workspaceId, docId, commentId, deletedAt: null },
|
||||||
|
orderBy: { sid: 'asc' },
|
||||||
|
})) as Reply[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a reply content
|
* Update a reply content
|
||||||
* @param input - The reply update input
|
* @param input - The reply update input
|
||||||
|
|||||||
Reference in New Issue
Block a user