feat(web): enhance notifications indicator in UPC (#950)

* feat(web): scaffold ui for notifications indicator

* refactor(web): poll for notification overview instead of subscription

* test: rm failing notifications.resolver test stub

* feat(web): pulse indicator when new notifications are received
This commit is contained in:
Pujit Mehrotra
2024-11-07 14:36:30 -05:00
committed by GitHub
parent 3fc41480a2
commit 0b469f5b3f
12 changed files with 208 additions and 55 deletions

View File

@@ -0,0 +1,98 @@
<script setup lang="ts">
import { useQuery } from "@vue/apollo-composable";
import { unreadOverview } from "./graphql/notification.query";
import { Importance } from "~/composables/gql/graphql";
import {
BellIcon,
ExclamationTriangleIcon,
ShieldExclamationIcon,
} from "@heroicons/vue/24/solid";
import { cn } from '~/components/shadcn/utils'
import { onWatcherCleanup } from "vue";
const { result } = useQuery(unreadOverview, null, {
pollInterval: 2_000, // 2 seconds
});
const overview = computed(() => {
if (!result.value) {
return;
}
return result.value.notifications.overview.unread;
});
const indicatorLevel = computed(() => {
if (!overview.value) {
return undefined;
}
switch (true) {
case overview.value.alert > 0:
return Importance.Alert;
case overview.value.warning > 0:
return Importance.Warning;
case overview.value.total > 0:
return "UNREAD";
default:
return undefined;
}
});
const icon = computed<{ component: Component; color: string } | null>(() => {
switch (indicatorLevel.value) {
case Importance.Warning:
return {
component: ExclamationTriangleIcon,
color: "text-yellow-500 translate-y-0.5",
};
case Importance.Alert:
return {
component: ShieldExclamationIcon,
color: "text-red-500",
};
}
return null;
});
/** whether new notifications ocurred */
const hasNewNotifications = ref(false);
// watch for new notifications, set a temporary indicator when they're reveived
watch(overview, (newVal, oldVal) => {
if (!newVal || !oldVal) {
return;
}
hasNewNotifications.value = newVal.total > oldVal.total;
// lifetime of 'new notification' state
const msToLive = 30_000;
const timeout = setTimeout(() => {
hasNewNotifications.value = false;
}, msToLive);
onWatcherCleanup(() => clearTimeout(timeout));
});
</script>
<template>
<div class="flex items-end gap-1">
<div class="relative">
<BellIcon class="w-6 h-6" />
<div
v-if="indicatorLevel"
:class="
cn('absolute top-0 right-0 size-2.5 rounded-full', {
'bg-unraid-red': indicatorLevel === Importance.Alert,
'bg-yellow-500': indicatorLevel === Importance.Warning,
'bg-green-500': indicatorLevel === 'UNREAD',
})
"
/>
<div
v-if="hasNewNotifications || indicatorLevel === Importance.Alert"
class="absolute top-0 right-0 size-2.5 rounded-full bg-unraid-red animate-ping"
/>
</div>
<component
:is="icon.component"
v-if="icon"
:class="cn('size-6', icon.color)"
/>
</div>
</template>

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import { BellIcon } from "@heroicons/vue/24/solid";
import {
Sheet,
SheetContent,
@@ -24,7 +23,7 @@ const importance = ref<Importance | undefined>(undefined);
<Sheet>
<SheetTrigger @click="determineTeleportTarget">
<span class="sr-only">Notifications</span>
<BellIcon class="w-6 h-6" />
<NotificationsIndicator />
</SheetTrigger>
<!-- We remove the horizontal padding from the container to keep the NotificationList's scrollbar in the right place -->

View File

@@ -13,6 +13,15 @@ export const NOTIFICATION_FRAGMENT = graphql(/* GraphQL */ `
}
`);
export const NOTIFICATION_COUNT_FRAGMENT = graphql(/* GraphQL */ `
fragment NotificationCountFragment on NotificationCounts {
total
info
warning
alert
}
`);
export const getNotifications = graphql(/* GraphQL */ `
query Notifications($filter: NotificationFilter!) {
notifications {
@@ -57,3 +66,18 @@ export const deleteNotification = graphql(/* GraphQL */ `
}
}
`);
export const unreadOverview = graphql(/* GraphQL */ `
query Overview {
notifications {
overview {
unread {
info
warning
alert
total
}
}
}
}
`);