mirror of
https://github.com/immich-app/immich.git
synced 2026-03-01 18:19:10 +03:00
feat: notifications (#17701)
* feat: notifications * UI works * chore: pr feedback * initial fetch and clear notification upon logging out * fix: merge --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
@@ -279,6 +279,26 @@ class AuthDeviceAccess {
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, notificationIds: Set<string>) {
|
||||
if (notificationIds.size === 0) {
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
.selectFrom('notifications')
|
||||
.select('notifications.id')
|
||||
.where('notifications.id', 'in', [...notificationIds])
|
||||
.where('notifications.userId', '=', userId)
|
||||
.execute()
|
||||
.then((stacks) => new Set(stacks.map((stack) => stack.id)));
|
||||
}
|
||||
}
|
||||
|
||||
class StackAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@@ -426,6 +446,7 @@ export class AccessRepository {
|
||||
asset: AssetAccess;
|
||||
authDevice: AuthDeviceAccess;
|
||||
memory: MemoryAccess;
|
||||
notification: NotificationAccess;
|
||||
person: PersonAccess;
|
||||
partner: PartnerAccess;
|
||||
stack: StackAccess;
|
||||
@@ -438,6 +459,7 @@ export class AccessRepository {
|
||||
this.asset = new AssetAccess(db);
|
||||
this.authDevice = new AuthDeviceAccess(db);
|
||||
this.memory = new MemoryAccess(db);
|
||||
this.notification = new NotificationAccess(db);
|
||||
this.person = new PersonAccess(db);
|
||||
this.partner = new PartnerAccess(db);
|
||||
this.stack = new StackAccess(db);
|
||||
|
||||
@@ -14,6 +14,7 @@ import { SystemConfig } from 'src/config';
|
||||
import { EventConfig } from 'src/decorators';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { NotificationDto } from 'src/dtos/notification.dto';
|
||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||
import { ImmichWorker, MetadataKey, QueueName } from 'src/enum';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
@@ -64,6 +65,7 @@ type EventMap = {
|
||||
'assets.restore': [{ assetIds: string[]; userId: string }];
|
||||
|
||||
'job.start': [QueueName, JobItem];
|
||||
'job.failed': [{ job: JobItem; error: Error | any }];
|
||||
|
||||
// session events
|
||||
'session.delete': [{ sessionId: string }];
|
||||
@@ -104,6 +106,7 @@ export interface ClientEventMap {
|
||||
on_server_version: [ServerVersionResponseDto];
|
||||
on_config_update: [];
|
||||
on_new_release: [ReleaseNotification];
|
||||
on_notification: [NotificationDto];
|
||||
on_session_delete: [string];
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { MediaRepository } from 'src/repositories/media.repository';
|
||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||
import { MoveRepository } from 'src/repositories/move.repository';
|
||||
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
@@ -55,6 +56,7 @@ export const repositories = [
|
||||
CryptoRepository,
|
||||
DatabaseRepository,
|
||||
DownloadRepository,
|
||||
EmailRepository,
|
||||
EventRepository,
|
||||
JobRepository,
|
||||
LibraryRepository,
|
||||
@@ -65,7 +67,7 @@ export const repositories = [
|
||||
MemoryRepository,
|
||||
MetadataRepository,
|
||||
MoveRepository,
|
||||
EmailRepository,
|
||||
NotificationRepository,
|
||||
OAuthRepository,
|
||||
PartnerRepository,
|
||||
PersonRepository,
|
||||
|
||||
103
server/src/repositories/notification.repository.ts
Normal file
103
server/src/repositories/notification.repository.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Insertable, Kysely, Updateable } from 'kysely';
|
||||
import { DateTime } from 'luxon';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
import { DB, Notifications } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { NotificationSearchDto } from 'src/dtos/notification.dto';
|
||||
|
||||
export class NotificationRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
cleanup() {
|
||||
return this.db
|
||||
.deleteFrom('notifications')
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
// remove soft-deleted notifications
|
||||
eb.and([eb('deletedAt', 'is not', null), eb('deletedAt', '<', DateTime.now().minus({ days: 3 }).toJSDate())]),
|
||||
|
||||
// remove old, read notifications
|
||||
eb.and([
|
||||
// keep recently read messages around for a few days
|
||||
eb('readAt', '>', DateTime.now().minus({ days: 2 }).toJSDate()),
|
||||
eb('createdAt', '<', DateTime.now().minus({ days: 15 }).toJSDate()),
|
||||
]),
|
||||
|
||||
eb.and([
|
||||
// remove super old, unread notifications
|
||||
eb('readAt', '=', null),
|
||||
eb('createdAt', '<', DateTime.now().minus({ days: 30 }).toJSDate()),
|
||||
]),
|
||||
]),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, {}] }, { name: 'unread', params: [DummyValue.UUID, { unread: true }] })
|
||||
search(userId: string, dto: NotificationSearchDto) {
|
||||
return this.db
|
||||
.selectFrom('notifications')
|
||||
.select(columns.notification)
|
||||
.where((qb) =>
|
||||
qb.and({
|
||||
userId,
|
||||
id: dto.id,
|
||||
level: dto.level,
|
||||
type: dto.type,
|
||||
readAt: dto.unread ? null : undefined,
|
||||
}),
|
||||
)
|
||||
.where('deletedAt', 'is', null)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
create(notification: Insertable<Notifications>) {
|
||||
return this.db
|
||||
.insertInto('notifications')
|
||||
.values(notification)
|
||||
.returning(columns.notification)
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
get(id: string) {
|
||||
return this.db
|
||||
.selectFrom('notifications')
|
||||
.select(columns.notification)
|
||||
.where('id', '=', id)
|
||||
.where('deletedAt', 'is not', null)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
update(id: string, notification: Updateable<Notifications>) {
|
||||
return this.db
|
||||
.updateTable('notifications')
|
||||
.set(notification)
|
||||
.where('deletedAt', 'is', null)
|
||||
.where('id', '=', id)
|
||||
.returning(columns.notification)
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
async updateAll(ids: string[], notification: Updateable<Notifications>) {
|
||||
await this.db.updateTable('notifications').set(notification).where('id', 'in', ids).execute();
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
await this.db
|
||||
.updateTable('notifications')
|
||||
.set({ deletedAt: DateTime.now().toJSDate() })
|
||||
.where('id', '=', id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteAll(ids: string[]) {
|
||||
await this.db
|
||||
.updateTable('notifications')
|
||||
.set({ deletedAt: DateTime.now().toJSDate() })
|
||||
.where('id', 'in', ids)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user