import React, { useCallback, useEffect, useRef, useState } from "react"; import { Alert, Box, Button, CircularProgress, Divider, IconButton, Popover, Stack, Tooltip, Typography, } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import DoneIcon from "@mui/icons-material/Done"; import { api, getApiErrorMessage } from "../api"; import { useI18n } from "../i18n/I18nProvider"; import { notificationDateLabel, UserNotification } from "../notifications"; type Props = { anchorEl: HTMLElement | null; onClose: () => void; onNavigate: (path: string) => void; onChanged: () => void; }; export default function NotificationsPopover({ anchorEl, onClose, onNavigate, onChanged }: Props) { const { t } = useI18n(); const open = Boolean(anchorEl); const [notifications, setNotifications] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [busyId, setBusyId] = useState(null); const activeLoad = useRef(null); const openGeneration = useRef(0); const load = useCallback(async () => { activeLoad.current?.abort(); const controller = new AbortController(); activeLoad.current = controller; setLoading(true); try { const response = await api.get("/notifications?limit=10", { signal: controller.signal }); if (controller.signal.aborted) return; setNotifications(response.data ?? []); setError(null); } catch (requestError) { if (!controller.signal.aborted) setError(getApiErrorMessage(requestError, t("notificationsLoadFailed"))); } finally { if (activeLoad.current === controller) setLoading(false); } }, [t]); useEffect(() => { openGeneration.current += 1; if (open) void load(); else { activeLoad.current?.abort(); setNotifications([]); setError(null); setBusyId(null); } return () => activeLoad.current?.abort(); }, [load, open]); const markRead = async (notification: UserNotification) => { if (notification.readAtUtc || busyId) return; const generation = openGeneration.current; setBusyId(notification.id); try { await api.post(`/notifications/${notification.id}/read`); if (generation !== openGeneration.current) return; setNotifications((items) => items.map((item) => item.id === notification.id ? { ...item, readAtUtc: new Date().toISOString() } : item)); setError(null); onChanged(); } catch (requestError) { if (generation === openGeneration.current) setError(getApiErrorMessage(requestError, t("notificationsActionFailed"))); } finally { if (generation === openGeneration.current) setBusyId(null); } }; const dismiss = async (notification: UserNotification) => { if (busyId) return; const generation = openGeneration.current; setBusyId(notification.id); try { await api.delete(`/notifications/${notification.id}`); if (generation !== openGeneration.current) return; setNotifications((items) => items.filter((item) => item.id !== notification.id)); setError(null); onChanged(); } catch (requestError) { if (generation === openGeneration.current) setError(getApiErrorMessage(requestError, t("notificationsActionFailed"))); } finally { if (generation === openGeneration.current) setBusyId(null); } }; const openNotification = async (notification: UserNotification) => { if (!notification.linkPath) return; const generation = openGeneration.current; if (!notification.readAtUtc) await markRead(notification); if (generation !== openGeneration.current) return; onClose(); onNavigate(notification.linkPath); }; return ( {t("notifications")} {t("notificationsRecent")} {loading ? : null} {error ? void load()}>{t("retry")}} sx={{ m: 0.5 }}>{error} : null} {!loading && !error && notifications.length === 0 ? ( {t("notificationsEmptyTitle")} {t("notificationsEmptyBody")} ) : null} {!loading && !error ? notifications.map((notification) => ( {notification.title} {!notification.readAtUtc ? {t("notificationsUnread")} : null} {notification.message} {notificationDateLabel(notification.createdAtUtc)} {notification.linkPath ? : null} {!notification.readAtUtc ? ( ) : null} )) : null} ); }