wrolpi/app/src/Events.js
lrnselfreliance 4e53e02519
Notifications without a URL are not links (#823)
Transcode, job, and Kiwix notifications send no URL, but every user notify
toast said "Click here to view the shared page" and opened /null on click.
Only a notification carrying a URL is followable now.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:37:26 -06:00

208 lines
6.2 KiB
JavaScript

import React, {useEffect} from 'react';
import {useRecurringTimeout} from "./hooks/customHooks";
import {ApiDownError, getEvents} from "./api";
import {toast} from "./components/ui";
const apiEventName = 'apiEvent';
function eventToast(title, description, type = 'info', time = 5000, onClick = null) {
toast({type: type, title: title, description: description, time: time, onClick: onClick});
}
export function handleEvents(events) {
const newestEvents = {};
events.forEach(e => {
const {event, message, subject, dt, url} = e;
document.dispatchEvent(new CustomEvent(apiEventName, {detail: e}));
console.debug(`Got event: ${event}`);
if (event === 'directory_refresh') {
eventToast(
'Refresh started',
message,
'info',
);
}
if (subject && newestEvents[subject] && newestEvents[subject] > dt) {
console.debug(`Already handled newer event of "${subject}".`);
return;
} else if (event === 'global_after_refresh_completed' || event === '') {
let message_ = message || 'Files have been refreshed.';
eventToast('Refresh completed', message_, 'success');
}
if (event === 'shutdown') {
eventToast('Shutdown', message, 'warning');
}
if (event === 'shutdown_failed') {
eventToast('Shutdown Failed', message, 'error');
}
if (event === 'downloads_disabled') {
eventToast('Downloads Disabled', message);
}
if (event === 'map_import_complete') {
eventToast('Map import completed', message, 'success');
}
if (event === 'map_import_failed') {
eventToast('Map import failed', message, 'error');
}
if (event === 'map_search_complete') {
eventToast('Map Search Index', message, 'success');
}
if (event === 'map_search_failed') {
eventToast('Map Search Index Failed', message, 'error');
}
if (event === 'user_notify_message') {
// Only a shared page carries a URL; other notifications (jobs, transcodes) are text.
if (url) {
eventToast(
message,
'Click here to view the shared page',
'info',
10_000,
() => window.open(url, '_self'));
} else {
eventToast(message, null, 'info', 10_000);
}
}
if (event === 'deleted') {
eventToast('Successful Delete', message, 'warning');
}
if (event === 'created') {
eventToast('Successful Creation', message, 'success');
}
if (event === 'tagged') {
eventToast('Successful tag', message, 'success');
}
if (event === 'file_move_completed') {
eventToast('Successful Move', message, 'success');
}
if (event === 'file_move_failed') {
eventToast('Move Failed', message, 'error');
}
if (event === 'config_import_failed') {
eventToast('Config Import Failed', message, 'error');
}
if (event === 'config_save_failed') {
eventToast('Config Save Failed', message, 'error');
}
if (event === 'upload_archive') {
eventToast(
'Archive Uploaded',
message,
'success',
5000,
() => window.open(url, '_self'));
}
if (event === 'upload_archive_failed') {
eventToast('Archive Upload Failed!', message, 'error', 5000);
}
if (event === 'screenshot_generated') {
eventToast(
'Screenshot Generated',
message,
'success',
5000,
() => window.open(url, '_self'));
}
if (event === 'screenshot_generation_failed') {
eventToast('Screenshot Generation Failed!', message, 'error', 5000);
}
if (event === 'upgrade_started') {
eventToast('Upgrade Started', message, 'info', 10000);
// Redirect to Controller UI for upgrade status after a short delay
setTimeout(() => {
window.location.href = '/controller/?upgrade=true';
}, 1000);
}
if (subject) {
newestEvents[subject] = dt;
}
})
}
export function useEventsInterval() {
const [events, setEvents] = React.useState(null);
// `now` is the last time we successfully fetched events.
const now = React.useRef(localStorage.getItem('events_now'));
const setNow = (newNow) => {
now.current = newNow;
localStorage.setItem('events_now', now.current);
}
const fetchEvents = async () => {
if (window.apiDown) { // apiDown is set in useStatus
return;
}
try {
const response = await getEvents(now.current);
setNow(response['now']);
setEvents(response['events']);
} catch (e) {
setEvents(null);
if (e instanceof ApiDownError) {
// API is down, do not log this error.
return;
}
// Ignore SyntaxError because they happen when the API is down.
if (!(e instanceof SyntaxError)) {
console.error(e);
}
}
}
useRecurringTimeout(fetchEvents, 1000 * 5);
useEffect(() => {
if (events && events.length >= 1) {
handleEvents(events);
}
}, [JSON.stringify(events)]);
return {events, fetchEvents}
}
export function useSubscribeEvents(listener) {
// Subscribe to all API sent events.
document.addEventListener(apiEventName, listener);
const unsubscribe = () => document.removeEventListener(apiEventName, listener);
useEffect(() => {
return unsubscribe;
}, []);
}
export function useSubscribeEventName(name, listener) {
// Subscribe to only API sent events that match `name`.
const localListener = (event) => {
if (event && event.detail['event'] === name) {
listener(event);
}
}
useSubscribeEvents(localListener);
}