refactor: notification queries (#32653)

(cherry picked from commit 8f3ba2c808e02e2a606b6acc65ce22f8ee3f9b34)
This commit is contained in:
Jason Rasmussen
2026-10-22 20:43:53 +03:00
committed by Codex
parent 3aa65809b1
commit fa77f168a0
5 changed files with 62 additions and 81 deletions
@@ -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));
</script>
@@ -143,7 +137,7 @@
<div
class="pointer-events-none absolute top-0 right-1 flex size-5 items-center justify-center rounded-full border bg-primary text-[10px] font-bold text-light"
>
{notificationManager.notifications.length}
{unreadCount}
</div>
{/if}
</div>
@@ -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) },
);
</script>
<div
@@ -104,7 +101,7 @@
{:else}
<Scrollable class="pb-6">
<Stack gap={0}>
{#each notificationManager.notifications as notification (notification.id)}
{#each notifications as notification (notification.id)}
<div animate:flip={{ duration: 400 }}>
<NotificationItem {notification} {onclick} />
</div>
+32
View File
@@ -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' },
},
});
@@ -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<NotificationDto[]>([]);
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();
+2 -2
View File
@@ -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 = () => {