Repository: toeverything/AFFiNE
File: packages/backend/server/src/core/notification/service.ts (TypeScript)

Code before:
  UnionNotificationBody,
} from '../../models';
import { DocReader } from '../doc';
import { WorkspaceBlobStorage } from '../storage';
import { generateDocPath } from '../utils/doc';

@Injectable()
export class NotificationService {
  private readonly logger = new Logger(NotificationService.name);

  constructor(
    private readonly models: Models,
    private readonly docReader: DocReader,
    private readonly workspaceBlobStorage: WorkspaceBlobStorage,
    private readonly mailer: MailService,
    private readonly url: URLHelper
  ) {}

  async cleanExpiredNotifications() {
    return await this.models.notification.cleanExpiredNotifications();
  }

  async createMention(input: MentionNotificationCreate) {
    const notification = await this.models.notification.createMention(input);
    // send email in background
    this.sendMentionEmail(input).catch(err => {
      this.logger.error(
        `Failed to send mention email to user ${input.userId}`,
        err
      );
    });
    return notification;
  }

  private async sendMentionEmail(input: MentionNotificationCreate) {
    const userSetting = await this.models.settings.get(input.userId);
    if (userSetting.receiveMentionEmail) {
      const receiver = await this.models.user.getWorkspaceUser(input.userId);
      if (!receiver) {
        return;
      }
      const user = await this.models.user.getWorkspaceUser(
        input.body.createdByUserId
      );
      if (!user) {
        return;
      }
      const doc = await this.models.doc.getMeta(
        input.body.workspaceId,
        input.body.doc.id
      );

Review comment (refactor):
The current method is very unfriendly for testing, making it difficult to simply determine whether an email has been sent, and need a sleep to wait.

@forehalo I'll wait for your `MailService` to be refactored before using the new method to send emails, as I don't want to write sleep code anymore :(

Code after:
import { generateDocPath } from '../utils/doc';

@Injectable()
export class NotificationService {
  private readonly logger = new Logger(NotificationService.name);

  constructor(
    private readonly models: Models,
    private readonly docReader: DocReader,
    private readonly workspaceBlobStorage: WorkspaceBlobStorage,
    private readonly mailer: Mailer,
    private readonly url: URLHelper
  ) {}

  async cleanExpiredNotifications() {
    return await this.models.notification.cleanExpiredNotifications();
  }

  async createMention(input: MentionNotificationCreate) {
    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: {