mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat: add album create and delete events with websocket integration
This commit is contained in:
parent
cfbc24579d
commit
186cc11692
6 changed files with 122 additions and 5 deletions
|
|
@ -51,6 +51,8 @@ type EventMap = {
|
||||||
// album events
|
// album events
|
||||||
AlbumUpdate: [{ id: string; recipientId: string }];
|
AlbumUpdate: [{ id: string; recipientId: string }];
|
||||||
AlbumInvite: [{ id: string; userId: string }];
|
AlbumInvite: [{ id: string; userId: string }];
|
||||||
|
AlbumDelete: [{ id: string; userId: string }];
|
||||||
|
AlbumCreate: [{ id: string; userId: string }];
|
||||||
|
|
||||||
// asset events
|
// asset events
|
||||||
AssetTag: [{ assetId: string }];
|
AssetTag: [{ assetId: string }];
|
||||||
|
|
@ -110,6 +112,8 @@ export interface ClientEventMap {
|
||||||
on_new_release: [ReleaseNotification];
|
on_new_release: [ReleaseNotification];
|
||||||
on_notification: [NotificationDto];
|
on_notification: [NotificationDto];
|
||||||
on_session_delete: [string];
|
on_session_delete: [string];
|
||||||
|
on_album_delete: [string];
|
||||||
|
on_album_create: [string];
|
||||||
|
|
||||||
AssetUploadReadyV1: [{ asset: SyncAssetV1; exif: SyncAssetExifV1 }];
|
AssetUploadReadyV1: [{ asset: SyncAssetV1; exif: SyncAssetExifV1 }];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,8 @@ export class AlbumService extends BaseService {
|
||||||
albumUsers,
|
albumUsers,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.eventRepository.emit('AlbumCreate', { id: album.id, userId: auth.user.id });
|
||||||
|
|
||||||
for (const { userId } of albumUsers) {
|
for (const { userId } of albumUsers) {
|
||||||
await this.eventRepository.emit('AlbumInvite', { id: album.id, userId });
|
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> {
|
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||||
await this.requireAccess({ auth, permission: Permission.AlbumDelete, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.AlbumDelete, ids: [id] });
|
||||||
await this.albumRepository.delete(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[]> {
|
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,18 @@ export class NotificationService extends BaseService {
|
||||||
await this.jobRepository.queue({ name: JobName.NotifyAlbumInvite, data: { id, recipientId: userId } });
|
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' })
|
@OnEvent({ name: 'SessionDelete' })
|
||||||
onSessionDelete({ sessionId }: ArgOf<'SessionDelete'>) {
|
onSessionDelete({ sessionId }: ArgOf<'SessionDelete'>) {
|
||||||
// after the response is sent
|
// after the response is sent
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
} from '$lib/stores/preferences.store';
|
} from '$lib/stores/preferences.store';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { userInteraction } from '$lib/stores/user.svelte';
|
import { userInteraction } from '$lib/stores/user.svelte';
|
||||||
|
import { websocketEvents } from '$lib/stores/websocket';
|
||||||
import { makeSharedLinkUrl } from '$lib/utils';
|
import { makeSharedLinkUrl } from '$lib/utils';
|
||||||
import {
|
import {
|
||||||
confirmAlbumDelete,
|
confirmAlbumDelete,
|
||||||
|
|
@ -36,11 +37,18 @@
|
||||||
import type { ContextMenuPosition } from '$lib/utils/context-menu';
|
import type { ContextMenuPosition } from '$lib/utils/context-menu';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { normalizeSearchString } from '$lib/utils/string-utils';
|
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 { modalManager } from '@immich/ui';
|
||||||
import { mdiDeleteOutline, mdiFolderDownloadOutline, mdiRenameOutline, mdiShareVariantOutline } from '@mdi/js';
|
import { mdiDeleteOutline, mdiFolderDownloadOutline, mdiRenameOutline, mdiShareVariantOutline } from '@mdi/js';
|
||||||
import { groupBy } from 'lodash-es';
|
import { groupBy } from 'lodash-es';
|
||||||
import { onMount, type Snippet } from 'svelte';
|
import { onDestroy, onMount, type Snippet } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { run } from 'svelte/legacy';
|
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) => {
|
const showAlbumContextMenu = (contextMenuDetail: ContextMenuPosition, album: AlbumResponseDto) => {
|
||||||
contextMenuTargetAlbum = album;
|
contextMenuTargetAlbum = album;
|
||||||
contextMenuPosition = {
|
contextMenuPosition = {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { userInteraction } from '$lib/stores/user.svelte';
|
import { userInteraction } from '$lib/stores/user.svelte';
|
||||||
|
import { websocketEvents } from '$lib/stores/websocket';
|
||||||
import { getAssetThumbnailUrl } from '$lib/utils';
|
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { getAllAlbums, type AlbumResponseDto } from '@immich/sdk';
|
import { getAlbumInfo, getAllAlbums, type AlbumResponseDto } from '@immich/sdk';
|
||||||
import { onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
let albums: AlbumResponseDto[] = $state([]);
|
let albums: AlbumResponseDto[] = $state([]);
|
||||||
|
let allAlbums: AlbumResponseDto[] = $state([]);
|
||||||
|
|
||||||
|
// Handle album deletion via websocket events
|
||||||
|
let unsubscribeWebsocket: (() => void) | undefined;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (userInteraction.recentAlbums) {
|
if (userInteraction.recentAlbums) {
|
||||||
|
|
@ -14,13 +19,51 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const allAlbums = await getAllAlbums({});
|
allAlbums = await getAllAlbums({});
|
||||||
albums = allAlbums.sort((a, b) => (a.updatedAt > b.updatedAt ? -1 : 1)).slice(0, 3);
|
albums = allAlbums.sort((a, b) => (a.updatedAt > b.updatedAt ? -1 : 1)).slice(0, 3);
|
||||||
userInteraction.recentAlbums = albums;
|
userInteraction.recentAlbums = albums;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('failed_to_load_assets'));
|
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>
|
</script>
|
||||||
|
|
||||||
{#each albums as album (album.id)}
|
{#each albums as album (album.id)}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ export interface Events {
|
||||||
on_new_release: (newRelase: ReleaseEvent) => void;
|
on_new_release: (newRelase: ReleaseEvent) => void;
|
||||||
on_session_delete: (sessionId: string) => void;
|
on_session_delete: (sessionId: string) => void;
|
||||||
on_notification: (notification: NotificationDto) => void;
|
on_notification: (notification: NotificationDto) => void;
|
||||||
|
on_album_delete: (albumId: string) => void;
|
||||||
|
on_album_create: (albumId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const websocket: Socket<Events> = io({
|
const websocket: Socket<Events> = io({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue