import { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { t as tr } from './i18n'; type Kind = 'success' | 'error'; type Notice = { id: number; message: string; kind: Kind }; let sequence = 0; const listeners = new Set<(notice: Notice) => void>(); export function showToast(message: string, kind: Kind) { if (message) { const notice = { id: ++sequence, message, kind }; listeners.forEach((fn) => fn(notice)); } } export function useToast(message: string, kind: Kind) { useEffect(() => { showToast(message, kind); }, [message, kind]); } function ToastItem({ notice, remove }: { notice: Notice; remove: (id: number) => void }) { useEffect(() => { const timer = setTimeout(() => remove(notice.id), notice.kind === 'error' ? 12000 : 6000); return () => clearTimeout(timer); }, [notice, remove]); return (

{tr(notice.message)}

); } export function ToastViewport() { const [notices, setNotices] = useState([]); const panel = useRef(null); const [host, setHost] = useState(document.body); useEffect(() => { // A native modal makes content outside it inert. Keep the live notification // and its dismiss control within the current dialog, while the popover // renders them above the dialog in the browser's top layer. const updateHost = () => { const dialogs = document.querySelectorAll( 'dialog[open], [role="dialog"][aria-modal="true"]', ); const next = dialogs.item(dialogs.length - 1) ?? document.body; setHost((current) => (current === next ? current : next)); }; const observer = new MutationObserver(updateHost); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['open'], }); updateHost(); return () => observer.disconnect(); }, []); const remove = useRef((id: number) => setNotices((rows) => rows.filter((n) => n.id !== id)), ).current; useEffect(() => { const receive = (n: Notice) => setNotices((rows) => [...rows.filter((v) => v.message !== n.message).slice(-2), n]); listeners.add(receive); return () => { listeners.delete(receive); }; }, []); useEffect(() => { const el = panel.current; if (!el) return; if (notices.length) { el.hidePopover?.(); el.showPopover?.(); } else el.hidePopover?.(); }, [notices, host]); return createPortal(
{notices.map((notice) => ( ))}
, host, ); }