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 = () => {