feat: local album events notification (#22817)

* feat: local album events notification

* pr feedback

* show number of unread notification
This commit is contained in:
Alex 2025-10-14 10:15:51 -05:00 committed by GitHub
parent 4d41fa08ad
commit d778286777
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 148 additions and 14 deletions

View file

@ -722,6 +722,8 @@ export enum NotificationType {
JobFailed = 'JobFailed',
BackupFailed = 'BackupFailed',
SystemMessage = 'SystemMessage',
AlbumInvite = 'AlbumInvite',
AlbumUpdate = 'AlbumUpdate',
Custom = 'Custom',
}

View file

@ -7,6 +7,7 @@ import { NotificationService } from 'src/services/notification.service';
import { INotifyAlbumUpdateJob } from 'src/types';
import { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub';
import { notificationStub } from 'test/fixtures/notification.stub';
import { userStub } from 'test/fixtures/user.stub';
import { newTestService, ServiceMocks } from 'test/utils';
@ -282,6 +283,7 @@ describe(NotificationService.name, () => {
},
],
});
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped);
});
@ -297,6 +299,7 @@ describe(NotificationService.name, () => {
},
],
});
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped);
});
@ -313,6 +316,7 @@ describe(NotificationService.name, () => {
],
});
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success);
@ -334,6 +338,7 @@ describe(NotificationService.name, () => {
],
});
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
@ -363,6 +368,7 @@ describe(NotificationService.name, () => {
],
});
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([
{ id: '1', type: AssetFileType.Thumbnail, path: 'path-to-thumb.jpg' },
@ -394,6 +400,7 @@ describe(NotificationService.name, () => {
],
});
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([assetStub.image.files[2]]);
@ -431,6 +438,7 @@ describe(NotificationService.name, () => {
albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser],
});
mocks.user.get.mockResolvedValueOnce(userStub.user1);
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
@ -453,6 +461,7 @@ describe(NotificationService.name, () => {
},
],
});
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
@ -475,6 +484,7 @@ describe(NotificationService.name, () => {
},
],
});
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
@ -489,6 +499,7 @@ describe(NotificationService.name, () => {
albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser],
});
mocks.user.get.mockResolvedValue(userStub.user1);
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);

View file

@ -1,5 +1,6 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { OnEvent, OnJob } from 'src/decorators';
import { MapAlbumDto } from 'src/dtos/album.dto';
import { mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import {
@ -295,6 +296,8 @@ export class NotificationService extends BaseService {
return JobStatus.Skipped;
}
await this.sendAlbumLocalNotification(album, recipientId, NotificationType.AlbumInvite, album.owner.name);
const { emailNotifications } = getPreferences(recipient.metadata);
if (!emailNotifications.enabled || !emailNotifications.albumInvite) {
@ -344,6 +347,8 @@ export class NotificationService extends BaseService {
return JobStatus.Skipped;
}
await this.sendAlbumLocalNotification(album, recipientId, NotificationType.AlbumUpdate);
const attachment = await this.getAlbumThumbnailAttachment(album);
const { server, templates } = await this.getConfig({ withCache: false });
@ -431,4 +436,25 @@ export class NotificationService extends BaseService {
cid: 'album-thumbnail',
};
}
private async sendAlbumLocalNotification(
album: MapAlbumDto,
userId: string,
type: NotificationType.AlbumInvite | NotificationType.AlbumUpdate,
senderName?: string,
) {
const isInvite = type === NotificationType.AlbumInvite;
const item = await this.notificationRepository.create({
userId,
type,
level: isInvite ? NotificationLevel.Success : NotificationLevel.Info,
title: isInvite ? 'Shared Album Invitation' : 'Shared Album Update',
description: isInvite
? `${senderName} shared an album (${album.albumName}) with you`
: `New media has been added to the album (${album.albumName})`,
data: JSON.stringify({ albumId: album.id }),
});
this.eventRepository.clientSend('on_notification', userId, mapNotification(item));
}
}