From 5b2314a11e5a783c820e4b109e393bb56d3932a5 Mon Sep 17 00:00:00 2001 From: rajchales-monito Date: Thu, 23 Jul 2026 23:42:36 +0200 Subject: [PATCH] Strengthen admin reply notifications --- public/admin.js | 91 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 17 deletions(-) diff --git a/public/admin.js b/public/admin.js index 94c263c..8d7732e 100644 --- a/public/admin.js +++ b/public/admin.js @@ -6,7 +6,11 @@ const state = { active: null, events: null, statusFilter: "active", - replyFiles: [] + replyFiles: [], + attentionTimer: null, + attentionOn: false, + originalTitle: document.title, + originalFavicon: document.querySelector("link[rel='icon']")?.href || "/favicon.svg" }; const $ = (selector) => document.querySelector(selector); @@ -41,6 +45,11 @@ $("#logoutButton").addEventListener("click", async () => { $("#settingsButton").addEventListener("click", () => $("#settingsPanel").classList.remove("hidden")); $("#closeSettingsButton").addEventListener("click", () => $("#settingsPanel").classList.add("hidden")); +window.addEventListener("focus", stopAttention); +document.addEventListener("visibilitychange", () => { + if (!document.hidden) stopAttention(); +}); + $("#onlineToggle").addEventListener("change", async () => { await saveSite({ isOnline: $("#onlineToggle").checked }); }); @@ -69,26 +78,31 @@ $("#settingsForm").addEventListener("submit", async (event) => { $("#replyForm").addEventListener("submit", async (event) => { event.preventDefault(); if (!state.activeId) return; - const textarea = event.currentTarget.querySelector("textarea[name='message']"); + const replyForm = event.currentTarget; + const textarea = replyForm.querySelector("textarea[name='message']"); const originalMessage = textarea.value; - const form = new FormData(event.currentTarget); + const form = new FormData(replyForm); const payload = { message: form.get("message") || "", attachments: await Promise.all(state.replyFiles.slice(0, 3).map(fileToPayload)) }; if (!payload.message.trim() && !payload.attachments.length) return; - textarea.value = ""; + clearReplyComposer(replyForm); try { await api(`/api/admin/conversations/${state.activeId}/messages`, { method: "POST", body: payload }); - event.currentTarget.reset(); + clearReplyComposer(replyForm); + setTimeout(() => clearReplyComposer(replyForm), 0); + setTimeout(() => clearReplyComposer(replyForm), 120); state.replyFiles = []; renderReplyAttachments(); await openConversation(state.activeId); + clearReplyComposer(replyForm); } catch (error) { textarea.value = originalMessage; + textarea.defaultValue = originalMessage; throw error; } }); @@ -207,6 +221,7 @@ async function loadConversations() { } async function openConversation(id) { + stopAttention(); state.activeId = id; const data = await api(`/api/admin/conversations/${id}`); state.active = data; @@ -234,7 +249,7 @@ function connectEvents() { return; } if (state.activeId) await openConversation(state.activeId).catch(() => clearActiveConversation()); - if (data.type === "conversation:new" || data.type === "message:new") notify(); + if (data.type === "conversation:new" || isVisitorMessageEvent(data)) notify(); }); } @@ -443,6 +458,15 @@ function renderReplyAttachments() { `).join(""); } +function clearReplyComposer(form = $("#replyForm")) { + const textarea = form?.querySelector("textarea[name='message']"); + if (!textarea) return; + textarea.value = ""; + textarea.defaultValue = ""; + textarea.textContent = ""; + textarea.dispatchEvent(new Event("input", { bubbles: true })); +} + function mergeImageFiles(currentFiles, incomingFiles) { const allowedTypes = new Set(["image/jpeg", "image/png", "image/webp", "image/gif", "image/avif"]); const nextFiles = [...currentFiles]; @@ -544,19 +568,52 @@ async function saveSite(patch) { } function notify() { - document.title = "* Nova zprava - MaalFlows"; - const audio = new AudioContext(); - const osc = audio.createOscillator(); + startAttention(); + playNotifySound(); +} + +function playNotifySound() { + const AudioCtor = window.AudioContext || window.webkitAudioContext; + if (!AudioCtor) return; + const audio = new AudioCtor(); const gain = audio.createGain(); - osc.frequency.value = 880; - gain.gain.value = 0.04; - osc.connect(gain); + gain.gain.value = 0.055; gain.connect(audio.destination); - osc.start(); - setTimeout(() => { - osc.stop(); - audio.close(); - }, 160); + [0, 0.14].forEach((offset, index) => { + const osc = audio.createOscillator(); + osc.type = "sine"; + osc.frequency.value = index ? 1040 : 820; + osc.connect(gain); + osc.start(audio.currentTime + offset); + osc.stop(audio.currentTime + offset + 0.11); + }); + setTimeout(() => audio.close().catch(() => {}), 520); +} + +function startAttention() { + if (state.attentionTimer) return; + const favicon = document.querySelector("link[rel='icon']"); + const alertIcon = `data:image/svg+xml,${encodeURIComponent('')}`; + state.attentionTimer = setInterval(() => { + state.attentionOn = !state.attentionOn; + document.title = state.attentionOn ? "* Nova zprava - MaalFlows" : state.originalTitle; + if (favicon) favicon.href = state.attentionOn ? alertIcon : state.originalFavicon; + }, 760); +} + +function stopAttention() { + if (state.attentionTimer) clearInterval(state.attentionTimer); + state.attentionTimer = null; + state.attentionOn = false; + document.title = state.originalTitle; + const favicon = document.querySelector("link[rel='icon']"); + if (favicon) favicon.href = state.originalFavicon; +} + +function isVisitorMessageEvent(data) { + if (data.type !== "message:new") return false; + const messages = data.payload?.messages || []; + return messages[messages.length - 1]?.senderType === "visitor"; } function flashTyping() {