mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
feat: local album events notification (#22817)
* feat: local album events notification * pr feedback * show number of unread notification
This commit is contained in:
parent
4d41fa08ad
commit
d778286777
10 changed files with 148 additions and 14 deletions
|
|
@ -19,6 +19,7 @@
|
|||
import { user } from '$lib/stores/user.store';
|
||||
import { Button, IconButton } from '@immich/ui';
|
||||
import { mdiBellBadge, mdiBellOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import ThemeButton from '../theme-button.svelte';
|
||||
import UserAvatar from '../user-avatar.svelte';
|
||||
|
|
@ -37,6 +38,14 @@
|
|||
let shouldShowNotificationPanel = $state(false);
|
||||
let innerWidth: number = $state(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>
|
||||
|
||||
<svelte:window bind:innerWidth />
|
||||
|
|
@ -125,15 +134,25 @@
|
|||
onEscape: () => (shouldShowNotificationPanel = false),
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
shape="round"
|
||||
color={hasUnreadNotifications ? 'primary' : 'secondary'}
|
||||
variant="ghost"
|
||||
size="medium"
|
||||
icon={hasUnreadNotifications ? mdiBellBadge : mdiBellOutline}
|
||||
onclick={() => (shouldShowNotificationPanel = !shouldShowNotificationPanel)}
|
||||
aria-label={$t('notifications')}
|
||||
/>
|
||||
<div class="relative">
|
||||
<IconButton
|
||||
shape="round"
|
||||
color={hasUnreadNotifications ? 'primary' : 'secondary'}
|
||||
variant="ghost"
|
||||
size="medium"
|
||||
icon={hasUnreadNotifications ? mdiBellBadge : mdiBellOutline}
|
||||
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}
|
||||
<NotificationPanel />
|
||||
|
|
|
|||
|
|
@ -1,12 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { NotificationLevel, NotificationType, type NotificationDto } from '@immich/sdk';
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
notification: NotificationDto;
|
||||
onclick: (id: string) => void;
|
||||
onclick: (notification: NotificationDto) => void;
|
||||
}
|
||||
|
||||
let { notification, onclick }: Props = $props();
|
||||
|
|
@ -62,6 +69,18 @@
|
|||
case NotificationType.Custom: {
|
||||
return mdiInformationOutline;
|
||||
}
|
||||
|
||||
case NotificationType.AlbumInvite: {
|
||||
return mdiImageAlbum;
|
||||
}
|
||||
|
||||
case NotificationType.AlbumUpdate: {
|
||||
return mdiImagePlus;
|
||||
}
|
||||
|
||||
default: {
|
||||
return mdiInformationOutline;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -83,7 +102,7 @@
|
|||
<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"
|
||||
type="button"
|
||||
onclick={() => onclick(notification.id)}
|
||||
onclick={() => onclick(notification)}
|
||||
title={notification.createdAt}
|
||||
>
|
||||
<div class="grid grid-cols-[56px_1fr_32px] items-center gap-2">
|
||||
|
|
@ -99,7 +118,7 @@
|
|||
</div>
|
||||
|
||||
<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}
|
||||
<Text class="overflow-hidden text-gray-600 dark:text-gray-300">{notification.description}</Text>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<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 {
|
||||
|
|
@ -8,6 +9,7 @@
|
|||
|
||||
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';
|
||||
|
|
@ -32,6 +34,37 @@
|
|||
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
|
||||
|
|
@ -71,7 +104,7 @@
|
|||
<Stack gap={0}>
|
||||
{#each notificationManager.notifications as notification (notification.id)}
|
||||
<div animate:flip={{ duration: 400 }}>
|
||||
<NotificationItem {notification} onclick={(id) => markAsRead(id)} />
|
||||
<NotificationItem {notification} {onclick} />
|
||||
</div>
|
||||
{/each}
|
||||
</Stack>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue