feat: add album create and delete events with websocket integration

This commit is contained in:
Pawel Wojtaszko 2025-08-07 22:33:56 +02:00
parent cfbc24579d
commit 186cc11692
6 changed files with 122 additions and 5 deletions

View file

@ -51,6 +51,8 @@ type EventMap = {
// album events
AlbumUpdate: [{ id: string; recipientId: string }];
AlbumInvite: [{ id: string; userId: string }];
AlbumDelete: [{ id: string; userId: string }];
AlbumCreate: [{ id: string; userId: string }];
// asset events
AssetTag: [{ assetId: string }];
@ -110,6 +112,8 @@ export interface ClientEventMap {
on_new_release: [ReleaseNotification];
on_notification: [NotificationDto];
on_session_delete: [string];
on_album_delete: [string];
on_album_create: [string];
AssetUploadReadyV1: [{ asset: SyncAssetV1; exif: SyncAssetExifV1 }];
}

View file

@ -121,6 +121,8 @@ export class AlbumService extends BaseService {
albumUsers,
);
await this.eventRepository.emit('AlbumCreate', { id: album.id, userId: auth.user.id });
for (const { userId } of albumUsers) {
await this.eventRepository.emit('AlbumInvite', { id: album.id, userId });
}
@ -154,6 +156,7 @@ export class AlbumService extends BaseService {
async delete(auth: AuthDto, id: string): Promise<void> {
await this.requireAccess({ auth, permission: Permission.AlbumDelete, ids: [id] });
await this.albumRepository.delete(id);
await this.eventRepository.emit('AlbumDelete', { id, userId: auth.user.id });
}
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {

View file

@ -211,6 +211,18 @@ export class NotificationService extends BaseService {
await this.jobRepository.queue({ name: JobName.NotifyAlbumInvite, data: { id, recipientId: userId } });
}
@OnEvent({ name: 'AlbumDelete' })
onAlbumDelete({ id, userId }: ArgOf<'AlbumDelete'>) {
console.log(`Album ${id} deleted by user ${userId}`);
this.eventRepository.clientSend('on_album_delete', userId, id);
}
@OnEvent({ name: 'AlbumCreate' })
onAlbumCreate({ id, userId }: ArgOf<'AlbumCreate'>) {
console.log(`Album ${id} created by user ${userId}`);
this.eventRepository.clientSend('on_album_create', userId, id);
}
@OnEvent({ name: 'SessionDelete' })
onSessionDelete({ sessionId }: ArgOf<'SessionDelete'>) {
// after the response is sent

View file

@ -24,6 +24,7 @@
} from '$lib/stores/preferences.store';
import { user } from '$lib/stores/user.store';
import { userInteraction } from '$lib/stores/user.svelte';
import { websocketEvents } from '$lib/stores/websocket';
import { makeSharedLinkUrl } from '$lib/utils';
import {
confirmAlbumDelete,
@ -36,11 +37,18 @@
import type { ContextMenuPosition } from '$lib/utils/context-menu';
import { handleError } from '$lib/utils/handle-error';
import { normalizeSearchString } from '$lib/utils/string-utils';
import { addUsersToAlbum, deleteAlbum, isHttpError, type AlbumResponseDto, type AlbumUserAddDto } from '@immich/sdk';
import {
addUsersToAlbum,
deleteAlbum,
getAlbumInfo,
isHttpError,
type AlbumResponseDto,
type AlbumUserAddDto,
} from '@immich/sdk';
import { modalManager } from '@immich/ui';
import { mdiDeleteOutline, mdiFolderDownloadOutline, mdiRenameOutline, mdiShareVariantOutline } from '@mdi/js';
import { groupBy } from 'lodash-es';
import { onMount, type Snippet } from 'svelte';
import { onDestroy, onMount, type Snippet } from 'svelte';
import { t } from 'svelte-i18n';
import { run } from 'svelte/legacy';
@ -210,6 +218,51 @@
}
});
// Handle album deletion via websocket events
let unsubscribeWebsocket: (() => void) | undefined;
onMount(() => {
unsubscribeWebsocket = websocketEvents.on('on_album_delete', (albumId) => {
// Remove the deleted album from both arrays
ownedAlbums = ownedAlbums.filter((album) => album.id !== albumId);
sharedAlbums = sharedAlbums.filter((album) => album.id !== albumId);
});
// Add listener for album creation
const unsubscribeCreate = websocketEvents.on('on_album_create', async (albumId) => {
try {
// Fetch the newly created album details
const newAlbum = await getAlbumInfo({ id: albumId });
// Add to owned albums if the current user is the owner
if (newAlbum.ownerId === $user?.id) {
ownedAlbums = [newAlbum, ...ownedAlbums];
} else {
// Check if current user is a shared user of this album
const isSharedWithCurrentUser = newAlbum.albumUsers.some((albumUser) => albumUser.user.id === $user?.id);
if (isSharedWithCurrentUser) {
sharedAlbums = [newAlbum, ...sharedAlbums];
}
}
} catch (error) {
console.error('Failed to fetch new album details:', error);
}
});
// Return a combined unsubscribe function
const originalUnsubscribe = unsubscribeWebsocket;
unsubscribeWebsocket = () => {
originalUnsubscribe?.();
unsubscribeCreate?.();
};
});
onDestroy(() => {
if (unsubscribeWebsocket) {
unsubscribeWebsocket();
}
});
const showAlbumContextMenu = (contextMenuDetail: ContextMenuPosition, album: AlbumResponseDto) => {
contextMenuTargetAlbum = album;
contextMenuPosition = {

View file

@ -1,12 +1,17 @@
<script lang="ts">
import { userInteraction } from '$lib/stores/user.svelte';
import { websocketEvents } from '$lib/stores/websocket';
import { getAssetThumbnailUrl } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { getAllAlbums, type AlbumResponseDto } from '@immich/sdk';
import { onMount } from 'svelte';
import { getAlbumInfo, getAllAlbums, type AlbumResponseDto } from '@immich/sdk';
import { onDestroy, onMount } from 'svelte';
import { t } from 'svelte-i18n';
let albums: AlbumResponseDto[] = $state([]);
let allAlbums: AlbumResponseDto[] = $state([]);
// Handle album deletion via websocket events
let unsubscribeWebsocket: (() => void) | undefined;
onMount(async () => {
if (userInteraction.recentAlbums) {
@ -14,13 +19,51 @@
return;
}
try {
const allAlbums = await getAllAlbums({});
allAlbums = await getAllAlbums({});
albums = allAlbums.sort((a, b) => (a.updatedAt > b.updatedAt ? -1 : 1)).slice(0, 3);
userInteraction.recentAlbums = albums;
} catch (error) {
handleError(error, $t('failed_to_load_assets'));
}
});
onMount(() => {
unsubscribeWebsocket = websocketEvents.on('on_album_delete', (albumId) => {
// Remove the deleted album from allAlbums
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) => {
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;
} catch (error) {
console.error('Failed to fetch new album details:', error);
}
});
// Return a combined unsubscribe function
const originalUnsubscribe = unsubscribeWebsocket;
unsubscribeWebsocket = () => {
originalUnsubscribe?.();
unsubscribeCreate?.();
};
});
onDestroy(() => {
if (unsubscribeWebsocket) {
unsubscribeWebsocket();
}
});
</script>
{#each albums as album (album.id)}

View file

@ -28,6 +28,8 @@ export interface Events {
on_new_release: (newRelase: ReleaseEvent) => void;
on_session_delete: (sessionId: string) => void;
on_notification: (notification: NotificationDto) => void;
on_album_delete: (albumId: string) => void;
on_album_create: (albumId: string) => void;
}
const websocket: Socket<Events> = io({