Files
BetterSEQTA-Plus/src/plugins/built-in/notificationCollector/index.ts
T
2025-11-10 17:10:53 +10:30

155 lines
4.4 KiB
TypeScript

import type { Plugin } from "../../core/types";
interface NotificationCollectorStorage {
lastNotificationCount: number;
lastCheckedTime: string;
consecutiveErrors: number;
}
const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
id: "notificationCollector",
name: "Notification Collector",
description: "Collects and displays SEQTA notifications",
version: "1.0.0",
settings: {},
disableToggle: true,
run: async (api) => {
let pollInterval: number | null = null;
let isVisible = !document.hidden;
let baseInterval = 30000; // 30 seconds
const maxInterval = 300000; // 5 minutes max
// Store last notification count in storage
if (!api.storage.lastNotificationCount) {
api.storage.lastNotificationCount = 0;
}
if (!api.storage.consecutiveErrors) {
api.storage.consecutiveErrors = 0;
}
const checkNotifications = async () => {
// Skip if tab is not visible to save battery
if (!isVisible) {
return;
}
try {
const alertDiv = document.querySelector(
"[class*='notifications__bubble___']",
) as HTMLElement;
if (alertDiv && api.storage.lastNotificationCount !== 0) {
alertDiv.textContent = api.storage.lastNotificationCount.toString();
}
const response = await fetch(
`${location.origin}/seqta/student/heartbeat?`,
{
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify({
timestamp: "1970-01-01 00:00:00.0",
hash: "#?page=/home",
}),
},
);
const data = await response.json();
// Store notification count for history
const notificationCount = data.payload.notifications.length;
api.storage.lastNotificationCount = notificationCount;
api.storage.lastCheckedTime = new Date().toISOString();
// Reset error count on success
api.storage.consecutiveErrors = 0;
if (alertDiv) {
alertDiv.textContent = notificationCount.toString();
} else {
console.info("[BetterSEQTA+] No notifications currently");
}
} catch (error) {
console.error("[BetterSEQTA+] Error fetching notifications:", error);
api.storage.consecutiveErrors =
(api.storage.consecutiveErrors || 0) + 1;
}
};
const getNextInterval = () => {
// Exponential backoff on errors, max 5 minutes
const errorMultiplier = Math.min(
Math.pow(2, api.storage.consecutiveErrors || 0),
10,
);
return Math.min(baseInterval * errorMultiplier, maxInterval);
};
const startPolling = () => {
if (pollInterval) return; // Already polling
checkNotifications();
const scheduleNext = () => {
const interval = getNextInterval();
pollInterval = window.setTimeout(() => {
checkNotifications().then(() => {
if (pollInterval) {
// Only continue if not stopped
scheduleNext();
}
});
}, interval);
};
scheduleNext();
};
const stopPolling = () => {
if (pollInterval) {
window.clearTimeout(pollInterval);
pollInterval = null;
const alertDiv = document.querySelector(
"[class*='notifications__bubble___']",
) as HTMLElement;
if (alertDiv) {
if (api.storage.lastNotificationCount > 9) {
alertDiv.textContent = "9+";
} else {
alertDiv.textContent = api.storage.lastNotificationCount.toString();
}
}
}
};
// Listen for visibility changes to pause/resume polling
const handleVisibilityChange = () => {
isVisible = !document.hidden;
if (isVisible && !pollInterval) {
// Resume polling when tab becomes visible
const alertDiv = document.querySelector(
"[class*='notifications__bubble___']",
);
if (alertDiv) {
startPolling();
}
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
api.seqta.onMount("[class*='notifications__bubble___']", (_) => {
startPolling();
});
return () => {
stopPolling();
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
},
};
export default notificationCollectorPlugin;