mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 04:48:53 +00:00
@@ -18,6 +18,7 @@ import {
|
||||
} from '../../../models';
|
||||
import { DocReader } from '../../doc';
|
||||
import { NotificationService } from '../service';
|
||||
|
||||
interface Context {
|
||||
module: TestingModule;
|
||||
notificationService: NotificationService;
|
||||
@@ -337,3 +338,46 @@ test('should raw doc title in mention notification if no doc found', async t =>
|
||||
t.is(body2.doc.title, 'doc-title-1');
|
||||
t.is(body2.doc.mode, DocMode.page);
|
||||
});
|
||||
|
||||
test('should send mention email by user setting', async t => {
|
||||
const { notificationService } = t.context;
|
||||
const docId = randomUUID();
|
||||
const notification = await notificationService.createMention({
|
||||
userId: member.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
createdByUserId: owner.id,
|
||||
doc: {
|
||||
id: docId,
|
||||
title: 'doc-title-1',
|
||||
blockId: 'block-id-1',
|
||||
mode: DocMode.page,
|
||||
},
|
||||
},
|
||||
});
|
||||
t.truthy(notification);
|
||||
// should send mention email
|
||||
const mentionMail = t.context.module.mails.last('Mention');
|
||||
t.is(mentionMail.to, member.email);
|
||||
|
||||
// update user setting to not receive mention email
|
||||
const mentionMailCount = t.context.module.mails.count('Mention');
|
||||
await t.context.models.settings.set(member.id, {
|
||||
receiveMentionEmail: false,
|
||||
});
|
||||
await notificationService.createMention({
|
||||
userId: member.id,
|
||||
body: {
|
||||
workspaceId: workspace.id,
|
||||
createdByUserId: owner.id,
|
||||
doc: {
|
||||
id: docId,
|
||||
title: 'doc-title-2',
|
||||
blockId: 'block-id-2',
|
||||
mode: DocMode.page,
|
||||
},
|
||||
},
|
||||
});
|
||||
// should not send mention email
|
||||
t.is(t.context.module.mails.count('Mention'), mentionMailCount);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { DocStorageModule } from '../doc';
|
||||
import { MailModule } from '../mail';
|
||||
import { PermissionModule } from '../permission';
|
||||
import { StorageModule } from '../storage';
|
||||
import { NotificationJob } from './job';
|
||||
@@ -8,7 +9,7 @@ import { NotificationResolver, UserNotificationResolver } from './resolver';
|
||||
import { NotificationService } from './service';
|
||||
|
||||
@Module({
|
||||
imports: [PermissionModule, DocStorageModule, StorageModule],
|
||||
imports: [PermissionModule, DocStorageModule, StorageModule, MailModule],
|
||||
providers: [
|
||||
UserNotificationResolver,
|
||||
NotificationResolver,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
|
||||
|
||||
import { NotificationNotFound, PaginationInput } from '../../base';
|
||||
import { NotificationNotFound, PaginationInput, URLHelper } from '../../base';
|
||||
import {
|
||||
InvitationNotificationCreate,
|
||||
MentionNotification,
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
UnionNotificationBody,
|
||||
} from '../../models';
|
||||
import { DocReader } from '../doc';
|
||||
import { Mailer } from '../mail';
|
||||
import { WorkspaceBlobStorage } from '../storage';
|
||||
import { generateDocPath } from '../utils/doc';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationService {
|
||||
@@ -20,7 +22,9 @@ export class NotificationService {
|
||||
constructor(
|
||||
private readonly models: Models,
|
||||
private readonly docReader: DocReader,
|
||||
private readonly workspaceBlobStorage: WorkspaceBlobStorage
|
||||
private readonly workspaceBlobStorage: WorkspaceBlobStorage,
|
||||
private readonly mailer: Mailer,
|
||||
private readonly url: URLHelper
|
||||
) {}
|
||||
|
||||
async cleanExpiredNotifications() {
|
||||
@@ -28,7 +32,48 @@ export class NotificationService {
|
||||
}
|
||||
|
||||
async createMention(input: MentionNotificationCreate) {
|
||||
return await this.models.notification.createMention(input);
|
||||
const notification = await this.models.notification.createMention(input);
|
||||
await this.sendMentionEmail(input);
|
||||
return notification;
|
||||
}
|
||||
|
||||
private async sendMentionEmail(input: MentionNotificationCreate) {
|
||||
const userSetting = await this.models.settings.get(input.userId);
|
||||
if (!userSetting.receiveMentionEmail) {
|
||||
return;
|
||||
}
|
||||
const receiver = await this.models.user.getWorkspaceUser(input.userId);
|
||||
if (!receiver) {
|
||||
return;
|
||||
}
|
||||
const doc = await this.models.doc.getMeta(
|
||||
input.body.workspaceId,
|
||||
input.body.doc.id
|
||||
);
|
||||
const title = doc?.title ?? input.body.doc.title;
|
||||
const url = this.url.link(
|
||||
generateDocPath({
|
||||
workspaceId: input.body.workspaceId,
|
||||
docId: input.body.doc.id,
|
||||
mode: input.body.doc.mode,
|
||||
blockId: input.body.doc.blockId,
|
||||
elementId: input.body.doc.elementId,
|
||||
})
|
||||
);
|
||||
await this.mailer.send({
|
||||
name: 'Mention',
|
||||
to: receiver.email,
|
||||
props: {
|
||||
user: {
|
||||
$$userId: input.body.createdByUserId,
|
||||
},
|
||||
doc: {
|
||||
title,
|
||||
url,
|
||||
},
|
||||
},
|
||||
});
|
||||
this.logger.log(`Mention email sent to user ${receiver.id}`);
|
||||
}
|
||||
|
||||
async createInvitation(input: InvitationNotificationCreate) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import test from 'ava';
|
||||
|
||||
import { DocID, DocVariant } from '../doc';
|
||||
import { DocMode } from '../../../models';
|
||||
import { DocID, DocVariant, generateDocPath } from '../doc';
|
||||
|
||||
test('can parse', t => {
|
||||
// workspace only
|
||||
@@ -80,3 +81,45 @@ test('special case: `wsId:space:page:pageId`', t => {
|
||||
t.throws(() => new DocID('ws:space:b:page'));
|
||||
t.throws(() => new DocID('ws:s:page:page'));
|
||||
});
|
||||
|
||||
test('should generate doc path', t => {
|
||||
t.is(
|
||||
generateDocPath({
|
||||
workspaceId: 'ws',
|
||||
docId: 'doc',
|
||||
mode: DocMode.page,
|
||||
}),
|
||||
'/workspace/ws/doc?mode=page'
|
||||
);
|
||||
|
||||
t.is(
|
||||
generateDocPath({
|
||||
workspaceId: 'ws',
|
||||
docId: 'doc',
|
||||
mode: DocMode.page,
|
||||
blockId: 'block',
|
||||
}),
|
||||
'/workspace/ws/doc?mode=page&blockIds=block'
|
||||
);
|
||||
|
||||
t.is(
|
||||
generateDocPath({
|
||||
workspaceId: 'ws',
|
||||
docId: 'doc',
|
||||
mode: DocMode.page,
|
||||
elementId: 'element.+?aaa$!@#',
|
||||
}),
|
||||
'/workspace/ws/doc?mode=page&elementIds=element.%2B%3Faaa%24%21%40%23'
|
||||
);
|
||||
|
||||
t.is(
|
||||
generateDocPath({
|
||||
workspaceId: 'ws',
|
||||
docId: 'doc',
|
||||
mode: DocMode.page,
|
||||
blockId: 'block',
|
||||
elementId: 'element',
|
||||
}),
|
||||
'/workspace/ws/doc?mode=page&elementIds=element&blockIds=block'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { DocMode } from '../../models';
|
||||
|
||||
export enum DocVariant {
|
||||
Workspace = 'workspace',
|
||||
Page = 'page',
|
||||
@@ -33,7 +35,7 @@ export class DocID {
|
||||
return this.variant === DocVariant.Workspace
|
||||
? this.workspace
|
||||
: // sub is always truthy when variant is not workspace
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
this.sub!;
|
||||
}
|
||||
|
||||
@@ -119,3 +121,29 @@ export class DocID {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type DocPathParams = {
|
||||
workspaceId: string;
|
||||
docId: string;
|
||||
mode: DocMode;
|
||||
blockId?: string;
|
||||
elementId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* To generate a doc url path like
|
||||
*
|
||||
* /workspace/{workspaceId}/{docId}?mode={DocMode}&elementIds={elementId}&blockIds={blockId}
|
||||
*/
|
||||
export function generateDocPath(params: DocPathParams) {
|
||||
const search = new URLSearchParams({
|
||||
mode: params.mode,
|
||||
});
|
||||
if (params.elementId) {
|
||||
search.set('elementIds', params.elementId);
|
||||
}
|
||||
if (params.blockId) {
|
||||
search.set('blockIds', params.blockId);
|
||||
}
|
||||
return `/workspace/${params.workspaceId}/${params.docId}?${search.toString()}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user