mirror of
https://github.com/immich-app/immich.git
synced 2025-11-02 21:27:39 +09:00
feat: local album events notification (#22817)
* feat: local album events notification * pr feedback * show number of unread notification
This commit is contained in:
6
mobile/openapi/lib/model/notification_type.dart
generated
6
mobile/openapi/lib/model/notification_type.dart
generated
@@ -26,6 +26,8 @@ class NotificationType {
|
|||||||
static const jobFailed = NotificationType._(r'JobFailed');
|
static const jobFailed = NotificationType._(r'JobFailed');
|
||||||
static const backupFailed = NotificationType._(r'BackupFailed');
|
static const backupFailed = NotificationType._(r'BackupFailed');
|
||||||
static const systemMessage = NotificationType._(r'SystemMessage');
|
static const systemMessage = NotificationType._(r'SystemMessage');
|
||||||
|
static const albumInvite = NotificationType._(r'AlbumInvite');
|
||||||
|
static const albumUpdate = NotificationType._(r'AlbumUpdate');
|
||||||
static const custom = NotificationType._(r'Custom');
|
static const custom = NotificationType._(r'Custom');
|
||||||
|
|
||||||
/// List of all possible values in this [enum][NotificationType].
|
/// List of all possible values in this [enum][NotificationType].
|
||||||
@@ -33,6 +35,8 @@ class NotificationType {
|
|||||||
jobFailed,
|
jobFailed,
|
||||||
backupFailed,
|
backupFailed,
|
||||||
systemMessage,
|
systemMessage,
|
||||||
|
albumInvite,
|
||||||
|
albumUpdate,
|
||||||
custom,
|
custom,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -75,6 +79,8 @@ class NotificationTypeTypeTransformer {
|
|||||||
case r'JobFailed': return NotificationType.jobFailed;
|
case r'JobFailed': return NotificationType.jobFailed;
|
||||||
case r'BackupFailed': return NotificationType.backupFailed;
|
case r'BackupFailed': return NotificationType.backupFailed;
|
||||||
case r'SystemMessage': return NotificationType.systemMessage;
|
case r'SystemMessage': return NotificationType.systemMessage;
|
||||||
|
case r'AlbumInvite': return NotificationType.albumInvite;
|
||||||
|
case r'AlbumUpdate': return NotificationType.albumUpdate;
|
||||||
case r'Custom': return NotificationType.custom;
|
case r'Custom': return NotificationType.custom;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
|
|||||||
@@ -12820,6 +12820,8 @@
|
|||||||
"JobFailed",
|
"JobFailed",
|
||||||
"BackupFailed",
|
"BackupFailed",
|
||||||
"SystemMessage",
|
"SystemMessage",
|
||||||
|
"AlbumInvite",
|
||||||
|
"AlbumUpdate",
|
||||||
"Custom"
|
"Custom"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|||||||
@@ -4650,6 +4650,8 @@ export enum NotificationType {
|
|||||||
JobFailed = "JobFailed",
|
JobFailed = "JobFailed",
|
||||||
BackupFailed = "BackupFailed",
|
BackupFailed = "BackupFailed",
|
||||||
SystemMessage = "SystemMessage",
|
SystemMessage = "SystemMessage",
|
||||||
|
AlbumInvite = "AlbumInvite",
|
||||||
|
AlbumUpdate = "AlbumUpdate",
|
||||||
Custom = "Custom"
|
Custom = "Custom"
|
||||||
}
|
}
|
||||||
export enum UserStatus {
|
export enum UserStatus {
|
||||||
|
|||||||
@@ -722,6 +722,8 @@ export enum NotificationType {
|
|||||||
JobFailed = 'JobFailed',
|
JobFailed = 'JobFailed',
|
||||||
BackupFailed = 'BackupFailed',
|
BackupFailed = 'BackupFailed',
|
||||||
SystemMessage = 'SystemMessage',
|
SystemMessage = 'SystemMessage',
|
||||||
|
AlbumInvite = 'AlbumInvite',
|
||||||
|
AlbumUpdate = 'AlbumUpdate',
|
||||||
Custom = 'Custom',
|
Custom = 'Custom',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { NotificationService } from 'src/services/notification.service';
|
|||||||
import { INotifyAlbumUpdateJob } from 'src/types';
|
import { INotifyAlbumUpdateJob } from 'src/types';
|
||||||
import { albumStub } from 'test/fixtures/album.stub';
|
import { albumStub } from 'test/fixtures/album.stub';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
|
import { notificationStub } from 'test/fixtures/notification.stub';
|
||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
import { newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
@@ -282,6 +283,7 @@ describe(NotificationService.name, () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||||
|
|
||||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped);
|
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped);
|
||||||
});
|
});
|
||||||
@@ -297,6 +299,7 @@ describe(NotificationService.name, () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||||
|
|
||||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped);
|
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped);
|
||||||
});
|
});
|
||||||
@@ -313,6 +316,7 @@ describe(NotificationService.name, () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
|
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
|
||||||
|
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||||
|
|
||||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success);
|
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success);
|
||||||
@@ -334,6 +338,7 @@ describe(NotificationService.name, () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
|
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
|
||||||
|
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||||
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
|
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
|
||||||
|
|
||||||
@@ -363,6 +368,7 @@ describe(NotificationService.name, () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
|
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
|
||||||
|
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||||
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([
|
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([
|
||||||
{ id: '1', type: AssetFileType.Thumbnail, path: 'path-to-thumb.jpg' },
|
{ id: '1', type: AssetFileType.Thumbnail, path: 'path-to-thumb.jpg' },
|
||||||
@@ -394,6 +400,7 @@ describe(NotificationService.name, () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
|
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
|
||||||
|
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||||
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([assetStub.image.files[2]]);
|
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([assetStub.image.files[2]]);
|
||||||
|
|
||||||
@@ -431,6 +438,7 @@ describe(NotificationService.name, () => {
|
|||||||
albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser],
|
albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser],
|
||||||
});
|
});
|
||||||
mocks.user.get.mockResolvedValueOnce(userStub.user1);
|
mocks.user.get.mockResolvedValueOnce(userStub.user1);
|
||||||
|
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||||
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
|
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
|
||||||
|
|
||||||
@@ -453,6 +461,7 @@ describe(NotificationService.name, () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||||
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
|
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
|
||||||
|
|
||||||
@@ -475,6 +484,7 @@ describe(NotificationService.name, () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||||
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
|
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
|
||||||
|
|
||||||
@@ -489,6 +499,7 @@ describe(NotificationService.name, () => {
|
|||||||
albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser],
|
albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser],
|
||||||
});
|
});
|
||||||
mocks.user.get.mockResolvedValue(userStub.user1);
|
mocks.user.get.mockResolvedValue(userStub.user1);
|
||||||
|
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||||
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
|
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { OnEvent, OnJob } from 'src/decorators';
|
import { OnEvent, OnJob } from 'src/decorators';
|
||||||
|
import { MapAlbumDto } from 'src/dtos/album.dto';
|
||||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import {
|
import {
|
||||||
@@ -295,6 +296,8 @@ export class NotificationService extends BaseService {
|
|||||||
return JobStatus.Skipped;
|
return JobStatus.Skipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.sendAlbumLocalNotification(album, recipientId, NotificationType.AlbumInvite, album.owner.name);
|
||||||
|
|
||||||
const { emailNotifications } = getPreferences(recipient.metadata);
|
const { emailNotifications } = getPreferences(recipient.metadata);
|
||||||
|
|
||||||
if (!emailNotifications.enabled || !emailNotifications.albumInvite) {
|
if (!emailNotifications.enabled || !emailNotifications.albumInvite) {
|
||||||
@@ -344,6 +347,8 @@ export class NotificationService extends BaseService {
|
|||||||
return JobStatus.Skipped;
|
return JobStatus.Skipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.sendAlbumLocalNotification(album, recipientId, NotificationType.AlbumUpdate);
|
||||||
|
|
||||||
const attachment = await this.getAlbumThumbnailAttachment(album);
|
const attachment = await this.getAlbumThumbnailAttachment(album);
|
||||||
|
|
||||||
const { server, templates } = await this.getConfig({ withCache: false });
|
const { server, templates } = await this.getConfig({ withCache: false });
|
||||||
@@ -431,4 +436,25 @@ export class NotificationService extends BaseService {
|
|||||||
cid: 'album-thumbnail',
|
cid: 'album-thumbnail',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async sendAlbumLocalNotification(
|
||||||
|
album: MapAlbumDto,
|
||||||
|
userId: string,
|
||||||
|
type: NotificationType.AlbumInvite | NotificationType.AlbumUpdate,
|
||||||
|
senderName?: string,
|
||||||
|
) {
|
||||||
|
const isInvite = type === NotificationType.AlbumInvite;
|
||||||
|
const item = await this.notificationRepository.create({
|
||||||
|
userId,
|
||||||
|
type,
|
||||||
|
level: isInvite ? NotificationLevel.Success : NotificationLevel.Info,
|
||||||
|
title: isInvite ? 'Shared Album Invitation' : 'Shared Album Update',
|
||||||
|
description: isInvite
|
||||||
|
? `${senderName} shared an album (${album.albumName}) with you`
|
||||||
|
: `New media has been added to the album (${album.albumName})`,
|
||||||
|
data: JSON.stringify({ albumId: album.id }),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventRepository.clientSend('on_notification', userId, mapNotification(item));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
server/test/fixtures/notification.stub.ts
vendored
Normal file
14
server/test/fixtures/notification.stub.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { NotificationLevel, NotificationType } from 'src/enum';
|
||||||
|
|
||||||
|
export const notificationStub = {
|
||||||
|
albumEvent: {
|
||||||
|
id: 'notification-album-event',
|
||||||
|
type: NotificationType.AlbumInvite,
|
||||||
|
description: 'You have been invited to a shared album',
|
||||||
|
title: 'Album Invitation',
|
||||||
|
createdAt: new Date('2024-01-01'),
|
||||||
|
data: { albumId: 'album-id' },
|
||||||
|
level: NotificationLevel.Success,
|
||||||
|
readAt: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { Button, IconButton } from '@immich/ui';
|
import { Button, IconButton } from '@immich/ui';
|
||||||
import { mdiBellBadge, mdiBellOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js';
|
import { mdiBellBadge, mdiBellOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import ThemeButton from '../theme-button.svelte';
|
import ThemeButton from '../theme-button.svelte';
|
||||||
import UserAvatar from '../user-avatar.svelte';
|
import UserAvatar from '../user-avatar.svelte';
|
||||||
@@ -37,6 +38,14 @@
|
|||||||
let shouldShowNotificationPanel = $state(false);
|
let shouldShowNotificationPanel = $state(false);
|
||||||
let innerWidth: number = $state(0);
|
let innerWidth: number = $state(0);
|
||||||
const hasUnreadNotifications = $derived(notificationManager.notifications.length > 0);
|
const hasUnreadNotifications = $derived(notificationManager.notifications.length > 0);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
await notificationManager.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load notifications on mount', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window bind:innerWidth />
|
<svelte:window bind:innerWidth />
|
||||||
@@ -125,15 +134,25 @@
|
|||||||
onEscape: () => (shouldShowNotificationPanel = false),
|
onEscape: () => (shouldShowNotificationPanel = false),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton
|
<div class="relative">
|
||||||
shape="round"
|
<IconButton
|
||||||
color={hasUnreadNotifications ? 'primary' : 'secondary'}
|
shape="round"
|
||||||
variant="ghost"
|
color={hasUnreadNotifications ? 'primary' : 'secondary'}
|
||||||
size="medium"
|
variant="ghost"
|
||||||
icon={hasUnreadNotifications ? mdiBellBadge : mdiBellOutline}
|
size="medium"
|
||||||
onclick={() => (shouldShowNotificationPanel = !shouldShowNotificationPanel)}
|
icon={hasUnreadNotifications ? mdiBellBadge : mdiBellOutline}
|
||||||
aria-label={$t('notifications')}
|
onclick={() => (shouldShowNotificationPanel = !shouldShowNotificationPanel)}
|
||||||
/>
|
aria-label={$t('notifications')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if hasUnreadNotifications}
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute border top-0 right-1 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-[10px] font-bold text-light"
|
||||||
|
>
|
||||||
|
{notificationManager.notifications.length}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if shouldShowNotificationPanel}
|
{#if shouldShowNotificationPanel}
|
||||||
<NotificationPanel />
|
<NotificationPanel />
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { NotificationLevel, NotificationType, type NotificationDto } from '@immich/sdk';
|
import { NotificationLevel, NotificationType, type NotificationDto } from '@immich/sdk';
|
||||||
import { IconButton, Stack, Text } from '@immich/ui';
|
import { IconButton, Stack, Text } from '@immich/ui';
|
||||||
import { mdiBackupRestore, mdiInformationOutline, mdiMessageBadgeOutline, mdiSync } from '@mdi/js';
|
import {
|
||||||
|
mdiBackupRestore,
|
||||||
|
mdiImageAlbum,
|
||||||
|
mdiImagePlus,
|
||||||
|
mdiInformationOutline,
|
||||||
|
mdiMessageBadgeOutline,
|
||||||
|
mdiSync,
|
||||||
|
} from '@mdi/js';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
notification: NotificationDto;
|
notification: NotificationDto;
|
||||||
onclick: (id: string) => void;
|
onclick: (notification: NotificationDto) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { notification, onclick }: Props = $props();
|
let { notification, onclick }: Props = $props();
|
||||||
@@ -62,6 +69,18 @@
|
|||||||
case NotificationType.Custom: {
|
case NotificationType.Custom: {
|
||||||
return mdiInformationOutline;
|
return mdiInformationOutline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case NotificationType.AlbumInvite: {
|
||||||
|
return mdiImageAlbum;
|
||||||
|
}
|
||||||
|
|
||||||
|
case NotificationType.AlbumUpdate: {
|
||||||
|
return mdiImagePlus;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return mdiInformationOutline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,7 +102,7 @@
|
|||||||
<button
|
<button
|
||||||
class="min-h-[80px] p-2 py-3 hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/10 border-b border-gray-200 dark:border-immich-dark-gray w-full"
|
class="min-h-[80px] p-2 py-3 hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/10 border-b border-gray-200 dark:border-immich-dark-gray w-full"
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => onclick(notification.id)}
|
onclick={() => onclick(notification)}
|
||||||
title={notification.createdAt}
|
title={notification.createdAt}
|
||||||
>
|
>
|
||||||
<div class="grid grid-cols-[56px_1fr_32px] items-center gap-2">
|
<div class="grid grid-cols-[56px_1fr_32px] items-center gap-2">
|
||||||
@@ -99,7 +118,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Stack class="text-left" gap={1}>
|
<Stack class="text-left" gap={1}>
|
||||||
<Text size="tiny" class="uppercase text-black dark:text-white font-semibold">{notification.title}</Text>
|
<Text size="tiny" class="text-black dark:text-white font-semibold text-base">{notification.title}</Text>
|
||||||
{#if notification.description}
|
{#if notification.description}
|
||||||
<Text class="overflow-hidden text-gray-600 dark:text-gray-300">{notification.description}</Text>
|
<Text class="overflow-hidden text-gray-600 dark:text-gray-300">{notification.description}</Text>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
import { focusTrap } from '$lib/actions/focus-trap';
|
import { focusTrap } from '$lib/actions/focus-trap';
|
||||||
import NotificationItem from '$lib/components/shared-components/navigation-bar/notification-item.svelte';
|
import NotificationItem from '$lib/components/shared-components/navigation-bar/notification-item.svelte';
|
||||||
import {
|
import {
|
||||||
@@ -8,6 +9,7 @@
|
|||||||
|
|
||||||
import { notificationManager } from '$lib/stores/notification-manager.svelte';
|
import { notificationManager } from '$lib/stores/notification-manager.svelte';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { NotificationType, type NotificationDto } from '@immich/sdk';
|
||||||
import { Button, Icon, Scrollable, Stack, Text } from '@immich/ui';
|
import { Button, Icon, Scrollable, Stack, Text } from '@immich/ui';
|
||||||
import { mdiBellOutline, mdiCheckAll } from '@mdi/js';
|
import { mdiBellOutline, mdiCheckAll } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@@ -32,6 +34,37 @@
|
|||||||
handleError(error, $t('errors.failed_to_update_notification_status'));
|
handleError(error, $t('errors.failed_to_update_notification_status'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNotificationAction = async (notification: NotificationDto) => {
|
||||||
|
switch (notification.type) {
|
||||||
|
case NotificationType.AlbumInvite:
|
||||||
|
case NotificationType.AlbumUpdate: {
|
||||||
|
if (!notification.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof notification.data !== 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(notification.data);
|
||||||
|
if (data?.albumId) {
|
||||||
|
await goto(`/albums/${data.albumId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onclick = async (notification: NotificationDto) => {
|
||||||
|
await markAsRead(notification.id);
|
||||||
|
await handleNotificationAction(notification);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -71,7 +104,7 @@
|
|||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
{#each notificationManager.notifications as notification (notification.id)}
|
{#each notificationManager.notifications as notification (notification.id)}
|
||||||
<div animate:flip={{ duration: 400 }}>
|
<div animate:flip={{ duration: 400 }}>
|
||||||
<NotificationItem {notification} onclick={(id) => markAsRead(id)} />
|
<NotificationItem {notification} {onclick} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
Reference in New Issue
Block a user