diff --git a/web/src/lib/components/shared-components/navigation-bar/NavigationBar.svelte b/web/src/lib/components/shared-components/navigation-bar/NavigationBar.svelte index 32ee08516..6b292ccf5 100644 --- a/web/src/lib/components/shared-components/navigation-bar/NavigationBar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/NavigationBar.svelte @@ -12,11 +12,11 @@ import { Route } from '$lib/route'; import { getGlobalActions } from '$lib/services/app.service'; import { mediaQueryManager } from '$lib/stores/media-query-manager.svelte'; - import { notificationManager } from '$lib/stores/notification-manager.svelte'; import { sidebarStore } from '$lib/stores/sidebar.svelte'; + import { getNotificationsQuery } from '$lib/queries/notification.query'; import { ActionButton, Button, IconButton, Logo } from '@immich/ui'; + import { createQuery } from '@tanstack/svelte-query'; import { mdiBellBadge, mdiBellOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js'; - import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; import ThemeButton from '../ThemeButton.svelte'; import UserAvatar from '../UserAvatar.svelte'; @@ -33,15 +33,9 @@ let shouldShowAccountInfoPanel = $state(false); 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); - } - }); + const query = createQuery(getNotificationsQuery); + const unreadCount = $derived(query.data?.length ?? 0); + const hasUnreadNotifications = $derived(unreadCount > 0); const { Cast } = $derived(getGlobalActions($t)); @@ -143,7 +137,7 @@
- {notificationManager.notifications.length} + {unreadCount}
{/if} diff --git a/web/src/lib/components/shared-components/navigation-bar/NotificationPanel.svelte b/web/src/lib/components/shared-components/navigation-bar/NotificationPanel.svelte index 4e6929a87..61de26a7d 100644 --- a/web/src/lib/components/shared-components/navigation-bar/NotificationPanel.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/NotificationPanel.svelte @@ -4,33 +4,28 @@ import NotificationItem from '$lib/components/shared-components/navigation-bar/NotificationItem.svelte'; import { OpenQueryParam } from '$lib/constants'; import { Route } from '$lib/route'; - import { notificationManager } from '$lib/stores/notification-manager.svelte'; - import { handleError } from '$lib/utils/handle-error'; + import { executeMutation } from '$lib/queries/client'; + import { + getNotificationsQuery, + updateNotificationMutation, + updateNotificationsMutation, + } from '$lib/queries/notification.query'; import { NotificationType, type NotificationDto } from '@immich/sdk'; - import { Button, Icon, Scrollable, Stack, Text, toastManager } from '@immich/ui'; + import { Button, Icon, Scrollable, Stack, Text } from '@immich/ui'; import { mdiBellOutline, mdiCheckAll } from '@mdi/js'; + import { createQuery } from '@tanstack/svelte-query'; import { t } from 'svelte-i18n'; import { flip } from 'svelte/animate'; import { fade } from 'svelte/transition'; - const noUnreadNotifications = $derived(notificationManager.notifications.length === 0); + const query = createQuery(getNotificationsQuery); + const notifications = $derived(query.data ?? []); + const noUnreadNotifications = $derived(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(); - toastManager.info($t('marked_all_as_read')); - } catch (error) { - handleError(error, $t('errors.failed_to_update_notification_status')); - } - }; + const markAllAsRead = () => + executeMutation(updateNotificationsMutation, [ + { notificationUpdateAllDto: { ids: notifications.map(({ id }) => id), readAt: new Date().toISOString() } }, + ]); const handleNotificationAction = async (notification: NotificationDto) => { switch (notification.type) { @@ -63,10 +58,12 @@ } }; - const onclick = async (notification: NotificationDto) => { - await markAsRead(notification.id); - await handleNotificationAction(notification); - }; + const onclick = (notification: NotificationDto) => + executeMutation( + updateNotificationMutation, + [{ id: notification.id, notificationUpdateDto: { readAt: new Date().toISOString() } }], + { onSuccess: () => void handleNotificationAction(notification) }, + );
- {#each notificationManager.notifications as notification (notification.id)} + {#each notifications as notification (notification.id)}
diff --git a/web/src/lib/queries/notification.query.ts b/web/src/lib/queries/notification.query.ts new file mode 100644 index 000000000..e4234bb8b --- /dev/null +++ b/web/src/lib/queries/notification.query.ts @@ -0,0 +1,32 @@ +import { getNotifications, updateNotification, updateNotifications } from '@immich/sdk'; +import { queryOptions } from '@tanstack/svelte-query'; +import { asMutation, queryClient } from '$lib/queries/client'; + +export const getNotificationsQuery = () => + queryOptions({ + queryKey: ['notifications'], + queryFn: ({ signal }) => getNotifications({ unread: true }, { signal }), + }); + +export const updateNotificationMutation = () => + asMutation(updateNotification, { + onSuccess: (notification) => + queryClient.setQueryData(getNotificationsQuery().queryKey, (notifications) => + notifications?.filter((item) => item.id !== notification.id || !notification.readAt), + ), + meta: { error: { toast: 'errors.failed_to_update_notification_status' } }, + }); + +export const updateNotificationsMutation = () => + asMutation(updateNotifications, { + onSuccess: (_, [{ notificationUpdateAllDto }]) => + queryClient.setQueryData(getNotificationsQuery().queryKey, (notifications) => + notifications?.filter( + (item) => !notificationUpdateAllDto.ids.includes(item.id) || !notificationUpdateAllDto.readAt, + ), + ), + meta: { + success: { toast: { color: 'info', description: 'marked_all_as_read' } }, + error: { toast: 'errors.failed_to_update_notification_status' }, + }, + }); diff --git a/web/src/lib/stores/notification-manager.svelte.ts b/web/src/lib/stores/notification-manager.svelte.ts deleted file mode 100644 index bc5d7529a..000000000 --- a/web/src/lib/stores/notification-manager.svelte.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { getNotifications, updateNotification, updateNotifications, type NotificationDto } from '@immich/sdk'; -import { t } from 'svelte-i18n'; -import { get } from 'svelte/store'; -import { eventManager } from '$lib/managers/event-manager.svelte'; -import { handleError } from '$lib/utils/handle-error'; - -class NotificationStore { - notifications = $state([]); - - constructor() { - eventManager.on({ - AuthLogin: () => this.refresh(), - AuthLogout: () => this.clear(), - }); - } - - async refresh() { - try { - this.notifications = await getNotifications({ unread: true }); - } catch (error) { - const translate = get(t); - handleError(error, translate('errors.failed_to_load_notifications')); - } - } - - markAsRead = async (id: string) => { - this.notifications = this.notifications.filter((notification) => notification.id !== id); - await updateNotification({ id, notificationUpdateDto: { readAt: new Date().toISOString() } }); - }; - - markAllAsRead = async () => { - const ids = this.notifications.map(({ id }) => id); - this.notifications = []; - await updateNotifications({ notificationUpdateAllDto: { ids, readAt: new Date().toISOString() } }); - }; - - clear = () => { - this.notifications = []; - }; -} - -export const notificationManager = new NotificationStore(); diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts index c8d759f3c..3a8d28c3f 100644 --- a/web/src/lib/stores/websocket.ts +++ b/web/src/lib/stores/websocket.ts @@ -15,11 +15,11 @@ import { page } from '$app/state'; import { authManager } from '$lib/managers/auth-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte'; import { queryClient } from '$lib/queries/client'; +import { getNotificationsQuery } from '$lib/queries/notification.query'; import { getServerConfigQuery } from '$lib/queries/server.query'; import { searchUsersAdminQuery } from '$lib/queries/user-admin.query'; import { Route } from '$lib/route'; import { maintenanceStore } from '$lib/stores/maintenance.store'; -import { notificationManager } from '$lib/stores/notification-manager.svelte'; import { createEventEmitter } from '$lib/utils/eventemitter'; interface AppRestartEvent { @@ -103,7 +103,7 @@ websocket .on('on_asset_trash', (assets) => eventManager.emit('AssetsDelete', assets)) .on('on_asset_update', (asset) => eventManager.emit('AssetUpdate', asset)) .on('on_person_thumbnail', (id) => eventManager.emit('PersonThumbnailReady', { id })) - .on('on_notification', () => notificationManager.refresh()) + .on('on_notification', () => void queryClient.invalidateQueries({ queryKey: getNotificationsQuery().queryKey })) .on('connect_error', (e) => console.log('Websocket Connect Error', e)); export const openWebsocketConnection = () => {