Make dashboard event toasts persistent

This commit is contained in:
Jamie Brunton committed 2026-08-14 09:08:13 +01:00
1 parent 4798385d8b
commit c75e71f26e
4 files changed
+91 -14

No files matched your search

+1 -1
View File
@@ -43,7 +43,7 @@ a.notification-entry:hover { background: var(--surface); }
.notification-empty > span { display: grid; place-items: center; width: 3.25rem; height: 3.25rem; margin-bottom: .9rem; color: var(--accent); background: var(--accent-dark); border: 1px solid #28593d; border-radius: 1rem; }
.notification-empty strong { margin-bottom: .35rem; font-size: 1rem; }
.notification-empty p { max-width: 18rem; margin: 0; color: var(--muted); font-size: .82rem; line-height: 1.5; }
.toast-stack { position: fixed; right: 1rem; bottom: 1rem; z-index: 75; display: grid; gap: .65rem; width: min(24rem, calc(100% - 2rem)); pointer-events: none; }
.toast-stack { position: fixed; right: 1rem; bottom: 1rem; z-index: 75; display: grid; gap: .65rem; width: min(24rem, calc(100% - 2rem)); max-height: calc(100vh - 2rem); overflow-y: auto; pointer-events: none; }
.notification-toast { position: relative; overflow: hidden; background: #15221c; border: 1px solid #355044; border-radius: .85rem; box-shadow: 0 1rem 3rem rgba(0, 0, 0, .4); pointer-events: auto; animation: notification-arrive 220ms ease-out both; }
.notification-toast.warning { background: #282112; border-color: #5b4922; }
.notification-toast.error { background: #2d1715; border-color: #69332e; }
+33 -12
View File
@@ -168,6 +168,34 @@ export function notificationForEvent(event: DashboardEvent): NotificationDraft |
}
}
/**
* Toasts interrupt the operator, so reserve them for inbound activity and
* states that need attention. Routine lifecycle and successful sync events
* remain available in the notification centre without producing a pop-up.
*/
export function shouldToastForEvent(event: DashboardEvent): boolean {
switch (event.type) {
case 'reply.received':
case 'media.received':
case 'message.failed':
case 'sync.failed':
return true;
case 'bot.status.changed':
return event.payload.status === 'stopped';
case 'rate-limit.changed':
return event.payload.limited;
case 'reddit.session.changed':
return ['expired', 'logged-out', 'error'].includes(event.payload.status);
case 'message.sent':
case 'conversation.updated':
case 'sync.started':
case 'sync.progress':
case 'sync.completed':
case 'log.created':
return false;
}
}
function loadNotifications(): NotificationItem[] {
const storage = notificationStorage();
if (!storage) return [];
@@ -221,20 +249,13 @@ export function NotificationProvider({ children, realtime: realtimeOverride }: N
setNotifications((current) => current.some((entry) => entry.id === item.id)
? current
: [item, ...current].slice(0, MAX_NOTIFICATIONS));
setToasts((current) => current.some((entry) => entry.id === item.id)
? current
: [...current, item].slice(-3));
if (shouldToastForEvent(event)) {
setToasts((current) => current.some((entry) => entry.id === item.id)
? current
: [...current, item]);
}
}), [realtime]);
useEffect(() => {
if (toasts.length === 0) return undefined;
const timers = toasts.map((toast) => globalThis.setTimeout(
() => dismissToast(toast.id),
toast.tone === 'error' ? 8_000 : 5_000,
));
return () => timers.forEach((timer) => globalThis.clearTimeout(timer));
}, [dismissToast, toasts]);
useEffect(() => {
function closeOnEscape(event: KeyboardEvent): void {
if (event.key === 'Escape') setOpen(false);