From e3d49bfa86bfdec7fc5f2ec71195598d00b177ab Mon Sep 17 00:00:00 2001 From: Pawel Wojtaszko Date: Wed, 13 Aug 2025 15:23:04 +0000 Subject: [PATCH] Updated recent albums updates --- server/src/repositories/event.repository.ts | 3 +- server/src/services/album.service.ts | 46 ++++++++++++++++--- server/src/services/notification.service.ts | 35 +++++++++++--- .../components/album-page/albums-list.svelte | 26 +++++++++++ .../side-bar/recent-albums.svelte | 34 ++++++-------- web/src/lib/stores/websocket.ts | 1 + web/src/lib/utils/album-websocket-utils.ts | 0 7 files changed, 110 insertions(+), 35 deletions(-) create mode 100644 web/src/lib/utils/album-websocket-utils.ts diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index abb6337d10..c9539f5c6a 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -49,7 +49,7 @@ type EventMap = { ConfigValidate: [{ newConfig: SystemConfig; oldConfig: SystemConfig }]; // album events - AlbumUpdate: [{ id: string; recipientId: string }]; + AlbumUpdate: [{ id: string; userId: string; notifyRecipients?: boolean }]; AlbumInvite: [{ id: string; userId: string }]; AlbumDelete: [{ id: string; userId: string }]; AlbumCreate: [{ id: string; userId: string }]; @@ -114,6 +114,7 @@ export interface ClientEventMap { on_session_delete: [string]; on_album_delete: [string]; on_album_create: [string]; + on_album_update: [string]; AssetUploadReadyV1: [{ asset: SyncAssetV1; exif: SyncAssetExifV1 }]; } diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index ffaf6489f8..409750f143 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -150,6 +150,12 @@ export class AlbumService extends BaseService { order: dto.order, }); + // Emit AlbumUpdate event with notifyRecipients flag for notification service to handle recipient lookup + await this.eventRepository.emit('AlbumUpdate', { + id: album.id, + userId: auth.user.id, + }); + return mapAlbumWithoutAssets({ ...updatedAlbum, assets: album.assets }); } @@ -177,13 +183,12 @@ export class AlbumService extends BaseService { albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId, }); - const allUsersExceptUs = [...album.albumUsers.map(({ user }) => user.id), album.owner.id].filter( - (userId) => userId !== auth.user.id, - ); - - for (const recipientId of allUsersExceptUs) { - await this.eventRepository.emit('AlbumUpdate', { id, recipientId }); - } + // Emit AlbumUpdate event with notifyRecipients flag for notification service to handle recipient lookup + await this.eventRepository.emit('AlbumUpdate', { + id, + userId: auth.user.id, + notifyRecipients: true, + }); } return results; @@ -204,6 +209,15 @@ export class AlbumService extends BaseService { await this.albumRepository.updateThumbnails(); } + // Emit AlbumUpdate event if any assets were successfully removed + if (removedIds.length > 0) { + await this.eventRepository.emit('AlbumUpdate', { + id, + userId: auth.user.id, + notifyRecipients: false, + }); + } + return results; } @@ -231,6 +245,12 @@ export class AlbumService extends BaseService { await this.eventRepository.emit('AlbumInvite', { id, userId }); } + // Emit AlbumUpdate event to notify all album members about new users being added + await this.eventRepository.emit('AlbumUpdate', { + id, + userId: auth.user.id, + }); + return this.findOrFail(id, { withAssets: true }).then(mapAlbumWithoutAssets); } @@ -256,11 +276,23 @@ export class AlbumService extends BaseService { } await this.albumUserRepository.delete({ albumsId: id, usersId: userId }); + + // Emit AlbumUpdate event to notify remaining album members about user removal + await this.eventRepository.emit('AlbumUpdate', { + id, + userId: auth.user.id, + }); } async updateUser(auth: AuthDto, id: string, userId: string, dto: UpdateAlbumUserDto): Promise { await this.requireAccess({ auth, permission: Permission.AlbumShare, ids: [id] }); await this.albumUserRepository.update({ albumsId: id, usersId: userId }, { role: dto.role }); + + // Emit AlbumUpdate event to notify all album members about role changes + await this.eventRepository.emit('AlbumUpdate', { + id, + userId: auth.user.id, + }); } private async findOrFail(id: string, options: AlbumInfoOptions) { diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index b1bbd7d3a6..0f92cd492b 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -198,12 +198,35 @@ export class NotificationService extends BaseService { } @OnEvent({ name: 'AlbumUpdate' }) - async onAlbumUpdate({ id, recipientId }: ArgOf<'AlbumUpdate'>) { - await this.jobRepository.removeJob(JobName.NotifyAlbumUpdate, `${id}/${recipientId}`); - await this.jobRepository.queue({ - name: JobName.NotifyAlbumUpdate, - data: { id, recipientId, delay: NotificationService.albumUpdateEmailDelayMs }, - }); + async onAlbumUpdate({ id, userId, notifyRecipients }: ArgOf<'AlbumUpdate'>) { + if (notifyRecipients) { + // Fetch album with users to get recipient list + const album = await this.albumRepository.getById(id, { withAssets: false }); + if (!album) { + this.logger.warn(`Album ${id} not found for update notification`); + return; + } + + // Get all users except the one who made the update + const allRecipients = [...album.albumUsers.map(({ user }) => user.id), album.owner.id].filter( + (recipientUserId) => recipientUserId !== userId, + ); + + // Send notifications and websocket events to all recipients + for (const recipient of allRecipients) { + await this.jobRepository.removeJob(JobName.NotifyAlbumUpdate, `${id}/${recipient}`); + await this.jobRepository.queue({ + name: JobName.NotifyAlbumUpdate, + data: { id, recipientId: recipient, delay: NotificationService.albumUpdateEmailDelayMs }, + }); + + // Send websocket event to the recipient + this.eventRepository.clientSend('on_album_update', recipient, id); + } + } + + // Always send websocket event to the user who made the update + this.eventRepository.clientSend('on_album_update', userId, id); } @OnEvent({ name: 'AlbumInvite' }) diff --git a/web/src/lib/components/album-page/albums-list.svelte b/web/src/lib/components/album-page/albums-list.svelte index 9445534916..98b663dba6 100644 --- a/web/src/lib/components/album-page/albums-list.svelte +++ b/web/src/lib/components/album-page/albums-list.svelte @@ -249,11 +249,37 @@ } }); + // Add listener for album updates + const unsubscribeUpdate = websocketEvents.on('on_album_update', async (albumId) => { + console.log(`Album update event received for album ID: ${albumId}`); + + try { + // Fetch the updated album details + const updatedAlbum = await getAlbumInfo({ id: albumId }); + + // Update the album in the appropriate array + const ownedIndex = ownedAlbums.findIndex((album) => album.id === albumId); + if (ownedIndex !== -1) { + ownedAlbums[ownedIndex] = updatedAlbum; + ownedAlbums = [...ownedAlbums]; // Trigger reactivity + } + + const sharedIndex = sharedAlbums.findIndex((album) => album.id === albumId); + if (sharedIndex !== -1) { + sharedAlbums[sharedIndex] = updatedAlbum; + sharedAlbums = [...sharedAlbums]; // Trigger reactivity + } + } catch (error) { + console.error('Failed to fetch updated album details:', error); + } + }); + // Return a combined unsubscribe function const originalUnsubscribe = unsubscribeWebsocket; unsubscribeWebsocket = () => { originalUnsubscribe?.(); unsubscribeCreate?.(); + unsubscribeUpdate?.(); }; }); diff --git a/web/src/lib/components/shared-components/side-bar/recent-albums.svelte b/web/src/lib/components/shared-components/side-bar/recent-albums.svelte index fc7ea5b58b..2b74fed4d2 100644 --- a/web/src/lib/components/shared-components/side-bar/recent-albums.svelte +++ b/web/src/lib/components/shared-components/side-bar/recent-albums.svelte @@ -14,10 +14,6 @@ let unsubscribeWebsocket: (() => void) | undefined; onMount(async () => { - if (userInteraction.recentAlbums) { - albums = userInteraction.recentAlbums; - return; - } try { allAlbums = await getAllAlbums({}); albums = allAlbums.sort((a, b) => (a.updatedAt > b.updatedAt ? -1 : 1)).slice(0, 3); @@ -28,34 +24,30 @@ }); onMount(() => { - unsubscribeWebsocket = websocketEvents.on('on_album_delete', (albumId) => { - // Remove the deleted album from allAlbums + const unsubscribeDelete = websocketEvents.on('on_album_delete', (albumId) => { allAlbums = allAlbums.filter((album) => album.id !== albumId); - // Update the displayed albums with the filtered and sorted result albums = allAlbums.sort((a, b) => (a.updatedAt > b.updatedAt ? -1 : 1)).slice(0, 3); userInteraction.recentAlbums = albums; }); - // Add listener for album creation - const unsubscribeCreate = websocketEvents.on('on_album_create', async (albumId) => { + const unsubscribeUpdate = websocketEvents.on('on_album_update', async (albumId) => { try { - // Fetch the newly created album details - const newAlbum = await getAlbumInfo({ id: albumId }); - // Add the new album to allAlbums - allAlbums = [newAlbum, ...allAlbums]; - // Update the displayed albums with the new sorted result - albums = allAlbums.sort((a, b) => (a.updatedAt > b.updatedAt ? -1 : 1)).slice(0, 3); - userInteraction.recentAlbums = albums; + const updatedAlbum = await getAlbumInfo({ id: albumId }); + + const index = allAlbums.findIndex((album) => album.id === albumId); + if (index !== -1) { + allAlbums[index] = updatedAlbum; + albums = allAlbums.sort((a, b) => (a.updatedAt > b.updatedAt ? -1 : 1)).slice(0, 3); + userInteraction.recentAlbums = albums; + } } catch (error) { - console.error('Failed to fetch new album details:', error); + console.error('Failed to fetch updated album details:', error); } }); - // Return a combined unsubscribe function - const originalUnsubscribe = unsubscribeWebsocket; unsubscribeWebsocket = () => { - originalUnsubscribe?.(); - unsubscribeCreate?.(); + unsubscribeDelete?.(); + unsubscribeUpdate?.(); }; }); diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts index 1f8e1321a7..7d064ee442 100644 --- a/web/src/lib/stores/websocket.ts +++ b/web/src/lib/stores/websocket.ts @@ -30,6 +30,7 @@ export interface Events { on_notification: (notification: NotificationDto) => void; on_album_delete: (albumId: string) => void; on_album_create: (albumId: string) => void; + on_album_update: (albumId: string) => void; } const websocket: Socket = io({ diff --git a/web/src/lib/utils/album-websocket-utils.ts b/web/src/lib/utils/album-websocket-utils.ts new file mode 100644 index 0000000000..e69de29bb2