diff --git a/packages/backend/server/src/__tests__/e2e/comment/resolver.spec.ts b/packages/backend/server/src/__tests__/e2e/comment/resolver.spec.ts index 406371b163..1aea976b36 100644 --- a/packages/backend/server/src/__tests__/e2e/comment/resolver.spec.ts +++ b/packages/backend/server/src/__tests__/e2e/comment/resolver.spec.ts @@ -688,7 +688,7 @@ 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 => { const docId = randomUUID(); await app.create(Mockers.DocUser, { @@ -740,12 +740,12 @@ e2e( 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, true); + t.is(notification.payload.isMention, undefined); } ); 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 => { const docId = randomUUID(); await app.create(Mockers.DocUser, { @@ -796,7 +796,140 @@ e2e( 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, true); + 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.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, 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); } ); diff --git a/packages/backend/server/src/core/comment/resolver.ts b/packages/backend/server/src/core/comment/resolver.ts index 620f1fb72f..fb8cac88a5 100644 --- a/packages/backend/server/src/core/comment/resolver.ts +++ b/packages/backend/server/src/core/comment/resolver.ts @@ -375,10 +375,7 @@ export class CommentResolver { reply?: Reply ) { const mentionUserIds = new Set(mentions); - // send comment mention notification to comment author on reply - if (reply) { - mentionUserIds.add(comment.userId); - } + const notifyUserIds = new Set(); // send comment mention notification to mentioned users for (const mentionUserId of mentionUserIds) { @@ -420,26 +417,41 @@ export class CommentResolver { comment.workspaceId, comment.docId ); - // if the owner is not in the mention user ids, send comment notification to the owner - if ( - owner && - owner.userId !== sender.id && - !mentionUserIds.has(owner.userId) - ) { - await this.queue.add('notification.sendComment', { - userId: owner.userId, - body: { - workspaceId: comment.workspaceId, - createdByUserId: sender.id, - commentId: comment.id, - replyId: reply?.id, - doc: { - id: comment.docId, - title: docTitle, - mode: docMode, + if (owner) { + notifyUserIds.add(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', { + userId, + body: { + workspaceId: comment.workspaceId, + createdByUserId: sender.id, + commentId: comment.id, + replyId: reply?.id, + doc: { + id: comment.docId, + title: docTitle, + mode: docMode, + }, }, - }, - }); + }); + } } } diff --git a/packages/backend/server/src/models/__tests__/comment.spec.ts b/packages/backend/server/src/models/__tests__/comment.spec.ts index 8ca1a81387..0ad1baeb27 100644 --- a/packages/backend/server/src/models/__tests__/comment.spec.ts +++ b/packages/backend/server/src/models/__tests__/comment.spec.ts @@ -524,3 +524,43 @@ test('should list changes', async t => { }); 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); +}); diff --git a/packages/backend/server/src/models/comment.ts b/packages/backend/server/src/models/comment.ts index d04d6fcaff..1f3d7b447e 100644 --- a/packages/backend/server/src/models/comment.ts +++ b/packages/backend/server/src/models/comment.ts @@ -300,6 +300,13 @@ export class CommentModel extends BaseModel { })) 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 * @param input - The reply update input