"use client"; import { useMemo } from "react"; import { Avatar, Badge, Card, Flex, Group, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core"; import { IconClock } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; import { useRequiredBoard } from "@homarr/boards/context"; import { useTimeAgo } from "@homarr/common"; import { useI18n } from "@homarr/translation/client"; import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url"; import { getUsableWidgetQueryData } from "../common/query-state"; import { WidgetQueryErrorIndicator } from "../common/query-state-indicator"; import type { WidgetComponentProps } from "../definition"; import { getNotificationDisplay } from "./display"; export default function NotificationsWidget({ options, integrationIds, width, height, displayMode, }: WidgetComponentProps<"notifications">) { const notificationsQuery = clientApi.widget.notifications.getNotifications.useQuery({ ...options, integrationIds, }); const notificationData = getUsableWidgetQueryData(notificationsQuery); const notificationIntegrations = useMemo(() => notificationData ?? [], [notificationData]); const { isPending } = notificationsQuery; const t = useI18n(); const board = useRequiredBoard(); const sortedNotifications = useMemo( () => notificationIntegrations .flatMap((integration) => integration.data.map((notification) => ({ ...notification, compositeKey: `${integration.integration.id}:${notification.id}`, integrationName: integration.integration.name, })), ) .sort((entryA, entryB) => entryB.time.getTime() - entryA.time.getTime()), [notificationIntegrations], ); const failedIntegrations = notificationIntegrations.filter( (integration): integration is typeof integration & { error: string } => Boolean(integration.error), ); const isDense = width < 280 || height < 180; const isRoomy = width >= 360 && height >= 220; const bodyLineClamp = height < 112 ? 1 : isDense ? 2 : height >= 300 ? 8 : 4; const notificationDisplay = getNotificationDisplay({ displayMode, hideLogos: options.hideLogos, isRoomy, bodyLineClamp, }); const columns = width >= 720 ? 2 : 1; const spacing = isRoomy ? "sm" : "xs"; return ( {notificationsQuery.error && ( )} {failedIntegrations.length > 0 && ( {failedIntegrations.map((integration) => ( {integration.integration.name}: {t("common.error")} ))} )} {isPending ? ( {t("common.action.loading")} ) : sortedNotifications.length > 0 ? ( {sortedNotifications.map((notification) => { const href = getSafeApplicationUrl(notification.href); return ( 1 ? undefined : "transparent"} style={{ color: "inherit", textDecoration: "none", borderBottom: columns > 1 ? undefined : "1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4))", }} > {notificationDisplay.showLogos && notification.source?.iconUrl && ( )} {notification.title && ( {notification.title} )} {notification.body} ); })} ) : ( {t("widget.notifications.noItems")} )} ); } const InfoDisplay = ({ date, source, dense }: { date: Date; source?: string; dense: boolean }) => { const timeAgo = useTimeAgo(date, 30000); // update every 30sec return ( {timeAgo} {source && •} {source && ( {source} )} ); };