Updated recent albums updates

This commit is contained in:
Pawel Wojtaszko 2025-08-13 15:23:04 +00:00
parent 7215e59809
commit e3d49bfa86
7 changed files with 110 additions and 35 deletions

View file

@ -49,7 +49,7 @@ type EventMap = {
ConfigValidate: [{ newConfig: SystemConfig; oldConfig: SystemConfig }]; ConfigValidate: [{ newConfig: SystemConfig; oldConfig: SystemConfig }];
// album events // album events
AlbumUpdate: [{ id: string; recipientId: string }]; AlbumUpdate: [{ id: string; userId: string; notifyRecipients?: boolean }];
AlbumInvite: [{ id: string; userId: string }]; AlbumInvite: [{ id: string; userId: string }];
AlbumDelete: [{ id: string; userId: string }]; AlbumDelete: [{ id: string; userId: string }];
AlbumCreate: [{ id: string; userId: string }]; AlbumCreate: [{ id: string; userId: string }];
@ -114,6 +114,7 @@ export interface ClientEventMap {
on_session_delete: [string]; on_session_delete: [string];
on_album_delete: [string]; on_album_delete: [string];
on_album_create: [string]; on_album_create: [string];
on_album_update: [string];
AssetUploadReadyV1: [{ asset: SyncAssetV1; exif: SyncAssetExifV1 }]; AssetUploadReadyV1: [{ asset: SyncAssetV1; exif: SyncAssetExifV1 }];
} }

View file

@ -150,6 +150,12 @@ export class AlbumService extends BaseService {
order: dto.order, 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 }); return mapAlbumWithoutAssets({ ...updatedAlbum, assets: album.assets });
} }
@ -177,13 +183,12 @@ export class AlbumService extends BaseService {
albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId, albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId,
}); });
const allUsersExceptUs = [...album.albumUsers.map(({ user }) => user.id), album.owner.id].filter( // Emit AlbumUpdate event with notifyRecipients flag for notification service to handle recipient lookup
(userId) => userId !== auth.user.id, await this.eventRepository.emit('AlbumUpdate', {
); id,
userId: auth.user.id,
for (const recipientId of allUsersExceptUs) { notifyRecipients: true,
await this.eventRepository.emit('AlbumUpdate', { id, recipientId }); });
}
} }
return results; return results;
@ -204,6 +209,15 @@ export class AlbumService extends BaseService {
await this.albumRepository.updateThumbnails(); 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; return results;
} }
@ -231,6 +245,12 @@ export class AlbumService extends BaseService {
await this.eventRepository.emit('AlbumInvite', { id, userId }); 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); 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 }); 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<void> { async updateUser(auth: AuthDto, id: string, userId: string, dto: UpdateAlbumUserDto): Promise<void> {
await this.requireAccess({ auth, permission: Permission.AlbumShare, ids: [id] }); await this.requireAccess({ auth, permission: Permission.AlbumShare, ids: [id] });
await this.albumUserRepository.update({ albumsId: id, usersId: userId }, { role: dto.role }); 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) { private async findOrFail(id: string, options: AlbumInfoOptions) {

View file

@ -198,12 +198,35 @@ export class NotificationService extends BaseService {
} }
@OnEvent({ name: 'AlbumUpdate' }) @OnEvent({ name: 'AlbumUpdate' })
async onAlbumUpdate({ id, recipientId }: ArgOf<'AlbumUpdate'>) { async onAlbumUpdate({ id, userId, notifyRecipients }: ArgOf<'AlbumUpdate'>) {
await this.jobRepository.removeJob(JobName.NotifyAlbumUpdate, `${id}/${recipientId}`); 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({ await this.jobRepository.queue({
name: JobName.NotifyAlbumUpdate, name: JobName.NotifyAlbumUpdate,
data: { id, recipientId, delay: NotificationService.albumUpdateEmailDelayMs }, 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' }) @OnEvent({ name: 'AlbumInvite' })

View file

@ -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 // Return a combined unsubscribe function
const originalUnsubscribe = unsubscribeWebsocket; const originalUnsubscribe = unsubscribeWebsocket;
unsubscribeWebsocket = () => { unsubscribeWebsocket = () => {
originalUnsubscribe?.(); originalUnsubscribe?.();
unsubscribeCreate?.(); unsubscribeCreate?.();
unsubscribeUpdate?.();
}; };
}); });

View file

@ -14,10 +14,6 @@
let unsubscribeWebsocket: (() => void) | undefined; let unsubscribeWebsocket: (() => void) | undefined;
onMount(async () => { onMount(async () => {
if (userInteraction.recentAlbums) {
albums = userInteraction.recentAlbums;
return;
}
try { try {
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);
@ -28,34 +24,30 @@
}); });
onMount(() => { onMount(() => {
unsubscribeWebsocket = websocketEvents.on('on_album_delete', (albumId) => { const unsubscribeDelete = websocketEvents.on('on_album_delete', (albumId) => {
// Remove the deleted album from allAlbums
allAlbums = allAlbums.filter((album) => album.id !== 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); albums = allAlbums.sort((a, b) => (a.updatedAt > b.updatedAt ? -1 : 1)).slice(0, 3);
userInteraction.recentAlbums = albums; userInteraction.recentAlbums = albums;
}); });
// Add listener for album creation const unsubscribeUpdate = websocketEvents.on('on_album_update', async (albumId) => {
const unsubscribeCreate = websocketEvents.on('on_album_create', async (albumId) => {
try { try {
// Fetch the newly created album details const updatedAlbum = await getAlbumInfo({ id: albumId });
const newAlbum = await getAlbumInfo({ id: albumId });
// Add the new album to allAlbums const index = allAlbums.findIndex((album) => album.id === albumId);
allAlbums = [newAlbum, ...allAlbums]; if (index !== -1) {
// Update the displayed albums with the new sorted result allAlbums[index] = updatedAlbum;
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) {
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 = () => { unsubscribeWebsocket = () => {
originalUnsubscribe?.(); unsubscribeDelete?.();
unsubscribeCreate?.(); unsubscribeUpdate?.();
}; };
}); });

View file

@ -30,6 +30,7 @@ export interface Events {
on_notification: (notification: NotificationDto) => void; on_notification: (notification: NotificationDto) => void;
on_album_delete: (albumId: string) => void; on_album_delete: (albumId: string) => void;
on_album_create: (albumId: string) => void; on_album_create: (albumId: string) => void;
on_album_update: (albumId: string) => void;
} }
const websocket: Socket<Events> = io({ const websocket: Socket<Events> = io({