Files
2026-07-13 13:32:57 +08:00

152 lines
4.1 KiB
TypeScript

import { useFetcher } from "@remix-run/react";
import { useCallback, useEffect, useRef, useState } from "react";
import simplur from "simplur";
import { NotificationIcon } from "~/assets/icons/NotificationIcon";
import { Button } from "~/components/primitives/Buttons";
import { Popover, PopoverContent, PopoverTrigger } from "~/components/primitives/Popover";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { usePlatformNotifications } from "~/routes/resources.platform-notifications";
import { NotificationCard } from "./NotificationCard";
type Notification = {
id: string;
friendlyId: string;
scope: string;
priority: number;
payload: {
version: string;
data: {
title: string;
description: string;
image?: string;
actionLabel?: string;
actionUrl?: string;
dismissOnAction?: boolean;
};
};
isRead: boolean;
};
export function NotificationPanel({
isCollapsed,
hasIncident,
organizationId,
projectId,
}: {
isCollapsed: boolean;
hasIncident: boolean;
organizationId: string;
projectId: string;
}) {
const { notifications } = usePlatformNotifications(organizationId, projectId) as {
notifications: Notification[];
};
const [dismissedIds, setDismissedIds] = useState<Set<string>>(new Set());
const dismissFetcher = useFetcher();
const seenIdsRef = useRef<Set<string>>(new Set());
const seenFetcher = useFetcher();
const clickedIdsRef = useRef<Set<string>>(new Set());
const clickFetcher = useFetcher();
const visibleNotifications = notifications.filter((n) => !dismissedIds.has(n.id));
const notification = visibleNotifications[0] ?? null;
const handleDismiss = useCallback((id: string) => {
setDismissedIds((prev) => new Set(prev).add(id));
dismissFetcher.submit(
{},
{
method: "POST",
action: `/resources/platform-notifications/${id}/dismiss`,
}
);
}, []);
const fireClickBeacon = useCallback((id: string) => {
if (clickedIdsRef.current.has(id)) return;
clickedIdsRef.current.add(id);
clickFetcher.submit(
{},
{
method: "POST",
action: `/resources/platform-notifications/${id}/clicked`,
}
);
}, []);
// Fire seen beacon
const fireSeenBeacon = useCallback((n: Notification) => {
if (seenIdsRef.current.has(n.id)) return;
seenIdsRef.current.add(n.id);
seenFetcher.submit(
{},
{
method: "POST",
action: `/resources/platform-notifications/${n.id}/seen`,
}
);
}, []);
// Beacon current notification on mount
useEffect(() => {
if (notification && !hasIncident) {
fireSeenBeacon(notification);
}
}, [notification?.id, hasIncident]);
if (!notification) {
return null;
}
const { title, description, image, actionUrl, dismissOnAction } = notification.payload.data;
const card = (
<NotificationCard
title={title}
description={description}
image={image}
actionUrl={actionUrl}
onDismiss={() => handleDismiss(notification.id)}
onCardClick={() => {
fireClickBeacon(notification.id);
if (dismissOnAction) {
handleDismiss(notification.id);
}
}}
onLinkClick={() => fireClickBeacon(notification.id)}
/>
);
return (
<Popover>
<div className={isCollapsed ? "p-1" : "p-2"}>
{isCollapsed ? (
<SimpleTooltip
asChild
button={
<div className="relative">
<PopoverTrigger asChild>
<Button variant="small-menu-item" className="h-8 w-8.75 justify-center">
<NotificationIcon className="size-5 text-success" />
</Button>
</PopoverTrigger>
</div>
}
content={simplur`${visibleNotifications.length} notification[|s]`}
side="right"
sideOffset={8}
disableHoverableContent
/>
) : (
card
)}
</div>
<PopoverContent side="right" sideOffset={8} align="end" className="w-56 min-w-0! p-0">
{card}
</PopoverContent>
</Popover>
);
}