This commit is contained in:
2026-10-21 13:32:30 -04:00
parent 1ff14c8d15
commit c5ad9bf7b8
4 changed files with 173 additions and 2 deletions
+136
View File
@@ -21,6 +21,135 @@ if (!presenceTab) {
}
let presenceQueue = Promise.resolve();
let lastPresenceUpdate = null;
const ALERTS_KEY = "thebutton-zone-alerts";
const ALERTS_SENT_KEY = "thebutton-zone-alerts-sent";
const ALERT_ZONES = ["blue", "yellow", "red", "black", "white"];
let alertSettings = readAlertSettings();
let alertObservation = null;
let alertAudio = null;
function readAlertSettings() {
try {
const value = JSON.parse(localStorage.getItem(ALERTS_KEY));
return { enabled: value?.enabled === true, zone: ALERT_ZONES.includes(value?.zone) ? value.zone : "blue",
sound: value?.sound === true, notification: value?.notification === true };
} catch {
return { enabled: false, zone: "blue", sound: false, notification: false };
}
}
function renderAlertSettings() {
$("alerts-enabled").checked = alertSettings.enabled;
$("alerts-zone").value = alertSettings.zone;
$("alerts-sound").checked = alertSettings.sound;
$("alerts-notification").checked = alertSettings.notification;
$("alerts-status").textContent = alertSettings.enabled
? `Watching for the ${alertSettings.zone} zone. Keep this page open.` : "Zone alerts are off.";
}
async function prepareAlertAudio() {
const Audio = window.AudioContext || window.webkitAudioContext;
if (!Audio) throw new Error("Sound is not supported by this browser.");
alertAudio = alertAudio || new Audio();
await alertAudio.resume();
if (alertAudio.state !== "running") throw new Error("Click Test chime to allow sound in this browser.");
}
function playAlertChime() {
if (!alertAudio || alertAudio.state !== "running") throw new Error("Click Test chime in Alerts to enable sound for this visit.");
const start = alertAudio.currentTime;
[660, 880].forEach((frequency, index) => {
const oscillator = alertAudio.createOscillator();
const gain = alertAudio.createGain();
const at = start + index * 0.18;
oscillator.frequency.value = frequency;
gain.gain.setValueAtTime(0, at);
gain.gain.linearRampToValueAtTime(0.08, at + 0.02);
gain.gain.exponentialRampToValueAtTime(0.001, at + 0.28);
oscillator.connect(gain);
gain.connect(alertAudio.destination);
oscillator.start(at);
oscillator.stop(at + 0.3);
oscillator.onended = () => { oscillator.disconnect(); gain.disconnect(); };
});
}
function observeZoneAlerts(snapshot) {
if (alertObservation && snapshot.now < alertObservation.now) return;
const remaining = snapshot.deadline - snapshot.now;
const zone = snapshot.zones.find((entry) => remaining > entry.remaining_to_ms)?.name;
const previous = alertObservation;
alertObservation = { now: snapshot.now, deadline: snapshot.deadline, zone, ended: snapshot.ended };
if (!alertSettings.enabled || !previous || snapshot.ended || previous.ended || remaining <= 0
|| previous.deadline !== snapshot.deadline || previous.zone === zone || zone !== alertSettings.zone) return;
const currentIndex = snapshot.zones.findIndex((entry) => entry.name === zone);
const previousIndex = snapshot.zones.findIndex((entry) => entry.name === previous.zone);
if (previousIndex < 0 || currentIndex <= previousIndex) return;
try {
const sent = JSON.parse(localStorage.getItem(ALERTS_SENT_KEY) || "null");
const zones = sent?.deadline === snapshot.deadline && Array.isArray(sent.zones) ? sent.zones : [];
if (zones.includes(zone)) return;
localStorage.setItem(ALERTS_SENT_KEY, JSON.stringify({ deadline: snapshot.deadline, zones: [...zones, zone] }));
} catch {
// The observation still prevents repeated alerts within this tab if storage is unavailable.
}
const message = `The countdown entered the ${zone} zone. ${formatPrecise(remaining)} remaining.`;
$("alerts-status").textContent = message;
$("alerts-error").textContent = "";
if (alertSettings.sound) {
try { playAlertChime(); } catch (error) { $("alerts-error").textContent = error.message; }
}
if (alertSettings.notification) {
try {
if (!("Notification" in window) || Notification.permission !== "granted") {
throw new Error("Browser notifications aren't permitted. Save your notification settings in Alerts to try again.");
}
const notification = new Notification("The Button: your zone is here", { body: message, tag: "thebutton-zone", silent: true });
notification.onclick = () => { window.focus(); notification.close(); };
} catch (error) { $("alerts-error").textContent = error.message; }
}
}
$("alerts-form").addEventListener("submit", async (event) => {
event.preventDefault();
$("alerts-error").textContent = "";
const settings = { enabled: $("alerts-enabled").checked, zone: $("alerts-zone").value,
sound: $("alerts-sound").checked, notification: $("alerts-notification").checked };
if (settings.enabled && !settings.sound && !settings.notification) {
$("alerts-error").textContent = "Choose a chime, a browser notification, or both.";
return;
}
let permissionRequest = null;
if (settings.enabled && settings.notification) {
if (!("Notification" in window) || !window.isSecureContext) {
$("alerts-error").textContent = "Browser notifications are unavailable here. Choose the chime instead.";
return;
}
// Start permission requests directly in the submit gesture, before awaiting audio.
try { permissionRequest = Notification.requestPermission().catch(() => "denied"); }
catch { permissionRequest = Promise.resolve("denied"); }
}
if (settings.enabled && settings.sound) {
try { await prepareAlertAudio(); } catch (error) { $("alerts-error").textContent = error.message; return; }
}
if (permissionRequest) {
const permission = await permissionRequest;
if (permission !== "granted") {
$("alerts-error").textContent = "Notifications were not allowed. Enable them in your browser settings, or choose the chime instead.";
return;
}
}
alertSettings = settings;
alertObservation = null; // baseline the next server response, never alert immediately on enabling
try { localStorage.setItem(ALERTS_KEY, JSON.stringify(settings)); }
catch { $("alerts-error").textContent = "Settings work for this visit but could not be saved in this browser."; }
renderAlertSettings();
});
$("alerts-test").addEventListener("click", async () => {
try { await prepareAlertAudio(); playAlertChime(); $("alerts-error").textContent = ""; }
catch (error) { $("alerts-error").textContent = error.message; }
});
renderAlertSettings();
function renderPresence(data) {
const count = data.online_count;
@@ -72,6 +201,11 @@ $("presence-opt-in").addEventListener("change", () => {
});
document.addEventListener("visibilitychange", syncPresence);
window.addEventListener("storage", (event) => {
if (event.key === ALERTS_KEY) {
alertSettings = readAlertSettings();
alertObservation = null;
renderAlertSettings();
}
if (event.key === PRESENCE_KEY || event.key === TOKEN_KEY) {
sharePresence = localStorage.getItem(PRESENCE_KEY) === "true";
token = localStorage.getItem(TOKEN_KEY);
@@ -85,6 +219,7 @@ function renderSections() {
"account-section": true,
"leaderboard-section": true,
"chat-section": true,
"alerts-section": true,
"stats-section": statsLoaded,
rules: true,
"share-controls": !!currentShareText,
@@ -254,6 +389,7 @@ async function refreshState() {
const first = !game;
clockOffset = s.now - (t0 + Date.now()) / 2;
game = s;
observeZoneAlerts(s);
$("history-global-summary").textContent =
`There have been ${s.total_presses.toLocaleString()} total presses and there are ${s.total_players.toLocaleString()} total players.`;
if (first) buildStaticUi();