Files
immich/web/src/lib/components/shared-components/navigation-bar/notification-panel.svelte
Alex d778286777 feat: local album events notification (#22817)
* feat: local album events notification

* pr feedback

* show number of unread notification
2025-10-14 10:15:51 -05:00

115 lines
3.6 KiB
Svelte

<script lang="ts">
import { goto } from '$app/navigation';
import { focusTrap } from '$lib/actions/focus-trap';
import NotificationItem from '$lib/components/shared-components/navigation-bar/notification-item.svelte';
import {
notificationController,
NotificationType as WebNotificationType,
} from '$lib/components/shared-components/notification/notification';
import { notificationManager } from '$lib/stores/notification-manager.svelte';
import { handleError } from '$lib/utils/handle-error';
import { NotificationType, type NotificationDto } from '@immich/sdk';
import { Button, Icon, Scrollable, Stack, Text } from '@immich/ui';
import { mdiBellOutline, mdiCheckAll } from '@mdi/js';
import { t } from 'svelte-i18n';
import { flip } from 'svelte/animate';
import { fade } from 'svelte/transition';
const noUnreadNotifications = $derived(notificationManager.notifications.length === 0);
const markAsRead = async (id: string) => {
try {
await notificationManager.markAsRead(id);
} catch (error) {
handleError(error, $t('errors.failed_to_update_notification_status'));
}
};
const markAllAsRead = async () => {
try {
await notificationManager.markAllAsRead();
notificationController.show({ message: $t('marked_all_as_read'), type: WebNotificationType.Info });
} catch (error) {
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>
<div
in:fade={{ duration: 100 }}
out:fade={{ duration: 100 }}
id="notification-panel"
class="absolute right-[25px] top-[70px] z-1 w-[min(360px,100vw-50px)] rounded-3xl bg-gray-100 border border-gray-200 shadow-lg dark:border dark:border-light dark:bg-immich-dark-gray text-light"
use:focusTrap
>
<Stack class="max-h-[500px]">
<div class="flex justify-between items-center mt-4 mx-4">
<Text size="medium" color="secondary" class="font-semibold">{$t('notifications')}</Text>
<div>
<Button
variant="ghost"
disabled={noUnreadNotifications}
leadingIcon={mdiCheckAll}
size="small"
color="primary"
onclick={() => markAllAsRead()}>{$t('mark_all_as_read')}</Button
>
</div>
</div>
<hr class="dark:border-black" />
{#if noUnreadNotifications}
<Stack
class="py-12 flex flex-col place-items-center place-content-center text-gray-700 dark:text-gray-300"
gap={1}
>
<Icon icon={mdiBellOutline} size="20"></Icon>
<Text>{$t('no_notifications')}</Text>
</Stack>
{:else}
<Scrollable class="pb-6">
<Stack gap={0}>
{#each notificationManager.notifications as notification (notification.id)}
<div animate:flip={{ duration: 400 }}>
<NotificationItem {notification} {onclick} />
</div>
{/each}
</Stack>
</Scrollable>
{/if}
</Stack>
</div>