From 1d6d4789931d9a026126b99169b8e8d3d0c3d316 Mon Sep 17 00:00:00 2001 From: rajchales-monito Date: Fri, 24 Jul 2026 14:43:38 +0200 Subject: [PATCH] Move notification sound to admin --- public/admin.css | 32 +++++++++++++++++++----- public/admin.html | 11 +++++++++ public/admin.js | 38 +++++++++++++++++++++++++++- public/widget.js | 63 ++++++----------------------------------------- 4 files changed, 82 insertions(+), 62 deletions(-) diff --git a/public/admin.css b/public/admin.css index 0130553..eb8acab 100644 --- a/public/admin.css +++ b/public/admin.css @@ -78,15 +78,15 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; } display: flex; align-items: center; justify-content: space-between; - gap: 10px; - padding: 14px; + gap: 6px; + padding: 10px; border-bottom: 1px solid var(--line); } -.brand h1 { margin: 0; font-size: 20px; } +.brand h1 { margin: 0; font-size: 18px; } .brand-actions { display: inline-flex; align-items: center; - gap: 8px; + gap: 5px; flex: 0 0 auto; } .brand button { background: #edf3ef; color: var(--ink); } @@ -102,10 +102,30 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; } height: 18px; fill: currentColor; } +.admin-sound-control { + height: 32px; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0 5px 0 0; + border-radius: 6px; + background: #edf3ef; +} +.admin-sound-control .sound-off { display: none; } +.admin-sound-control .muted .sound-on { display: none; } +.admin-sound-control .muted .sound-off { display: block; } +.admin-sound-control input[type="range"] { + width: 42px; + height: 4px; + padding: 0; + border: 0; + accent-color: var(--brand); + cursor: pointer; +} .logout-compact { height: 32px; - padding: 0 10px; - font-size: 12px; + padding: 0 8px; + font-size: 11px; } .online-dot-toggle { width: 28px; diff --git a/public/admin.html b/public/admin.html index f22d2aa..7e23d3c 100644 --- a/public/admin.html +++ b/public/admin.html @@ -33,6 +33,17 @@ +
+ + +
diff --git a/public/admin.js b/public/admin.js index 6318423..1b1cd13 100644 --- a/public/admin.js +++ b/public/admin.js @@ -9,6 +9,8 @@ const state = { events: null, statusFilter: "active", hideBots: localStorage.getItem("mf_admin_hide_bots") !== "0", + soundMuted: localStorage.getItem("mf_admin_sound_muted") === "1", + soundVolume: Number(localStorage.getItem("mf_admin_sound_volume") || 70), replyFiles: [], attentionTimer: null, attentionOn: false, @@ -19,6 +21,7 @@ const state = { const $ = (selector) => document.querySelector(selector); boot(); +renderAdminSoundControls(); async function boot() { const me = await api("/api/admin/me").catch(() => null); @@ -47,6 +50,20 @@ $("#logoutButton").addEventListener("click", async () => { $("#settingsButton").addEventListener("click", () => $("#settingsPanel").classList.remove("hidden")); $("#closeSettingsButton").addEventListener("click", () => $("#settingsPanel").classList.add("hidden")); +$("#adminSoundToggle").addEventListener("click", () => { + state.soundMuted = !state.soundMuted; + if (!state.soundMuted && state.soundVolume <= 0) state.soundVolume = 70; + localStorage.setItem("mf_admin_sound_muted", state.soundMuted ? "1" : "0"); + localStorage.setItem("mf_admin_sound_volume", String(state.soundVolume)); + renderAdminSoundControls(); +}); +$("#adminSoundVolume").addEventListener("input", (event) => { + state.soundVolume = clamp(Number(event.target.value), 0, 100); + state.soundMuted = state.soundVolume <= 0; + localStorage.setItem("mf_admin_sound_volume", String(state.soundVolume)); + localStorage.setItem("mf_admin_sound_muted", state.soundMuted ? "1" : "0"); + renderAdminSoundControls(); +}); $("#settingsForm").brand.addEventListener("input", (event) => { $("#settingsForm").brandHex.value = normalizeHexColor(event.target.value) || event.target.value; }); @@ -775,12 +792,26 @@ function notify() { playNotifySound(); } +function renderAdminSoundControls() { + const toggle = $("#adminSoundToggle"); + const volume = $("#adminSoundVolume"); + if (!toggle || !volume) return; + const safeVolume = clamp(state.soundVolume, 0, 100); + toggle.classList.toggle("muted", state.soundMuted || safeVolume <= 0); + toggle.setAttribute("aria-label", state.soundMuted ? "Zapnout zvuk notifikaci" : "Vypnout zvuk notifikaci"); + toggle.title = state.soundMuted ? "Zapnout zvuk notifikaci" : "Vypnout zvuk notifikaci"; + volume.value = String(safeVolume); + volume.title = `Hlasitost ${safeVolume}%`; +} + function playNotifySound() { + const volume = clamp(state.soundVolume, 0, 100); + if (state.soundMuted || volume <= 0) return; const AudioCtor = window.AudioContext || window.webkitAudioContext; if (!AudioCtor) return; const audio = new AudioCtor(); const gain = audio.createGain(); - gain.gain.value = 0.055; + gain.gain.value = 0.12 * (volume / 100); gain.connect(audio.destination); [0, 0.14].forEach((offset, index) => { const osc = audio.createOscillator(); @@ -793,6 +824,11 @@ function playNotifySound() { setTimeout(() => audio.close().catch(() => {}), 520); } +function clamp(value, min, max) { + if (!Number.isFinite(value)) return min; + return Math.min(max, Math.max(min, value)); +} + function startAttention() { if (state.attentionTimer) return; const favicon = document.querySelector("link[rel='icon']"); diff --git a/public/widget.js b/public/widget.js index e8b2546..f72e3bd 100644 --- a/public/widget.js +++ b/public/widget.js @@ -66,7 +66,7 @@ const panel = root.querySelector(".mf-panel"); const launcher = root.querySelector(".mf-launcher"); const close = root.querySelector(".mf-close"); - const { soundButton, infoButton } = createHeaderActions(root, close); + const { infoButton } = createHeaderActions(root, close); const form = root.querySelector(".mf-form"); const messages = root.querySelector(".mf-messages"); const typing = root.querySelector(".mf-typing"); @@ -77,7 +77,6 @@ unreadAdminCount: 0, knownAdminMessageId: 0 }; - renderSoundState(soundButton); launcher.addEventListener("click", () => { panel.classList.toggle("open"); @@ -91,11 +90,6 @@ } }); close.addEventListener("click", () => panel.classList.remove("open")); - soundButton.addEventListener("click", () => { - session.soundMuted = !session.soundMuted; - saveSession(); - renderSoundState(soundButton); - }); infoButton.addEventListener("click", () => { root.querySelector(".mf-gdpr").hidden = !root.querySelector(".mf-gdpr").hidden; }); @@ -234,17 +228,6 @@ function createHeaderActions(root, closeButton) { const actions = document.createElement("div"); actions.className = "mf-header-actions"; - const soundButton = document.createElement("button"); - soundButton.className = "mf-sound"; - soundButton.type = "button"; - soundButton.innerHTML = ` - - - `; const infoButton = document.createElement("button"); infoButton.className = "mf-info"; infoButton.type = "button"; @@ -255,38 +238,13 @@ closeButton.setAttribute("aria-label", "Zmensit chat"); closeButton.title = "Zmensit chat"; closeButton.before(actions); - actions.append(soundButton, infoButton, closeButton); + actions.append(infoButton, closeButton); const gdpr = document.createElement("p"); gdpr.className = "mf-gdpr"; gdpr.hidden = true; gdpr.textContent = "Odeslanim zpravy nam predavate udaje potrebne pro odpoved. Historii chatu uchovavame pro zakaznickou podporu."; root.querySelector(".mf-form").prepend(gdpr); - return { soundButton, infoButton }; - } - - function renderSoundState(button) { - button.classList.toggle("muted", Boolean(session.soundMuted)); - button.setAttribute("aria-label", session.soundMuted ? "Zapnout zvuk" : "Vypnout zvuk"); - button.title = session.soundMuted ? "Zapnout zvuk" : "Vypnout zvuk"; - } - - function playNotifySound() { - if (session.soundMuted) return; - const AudioCtor = window.AudioContext || window.webkitAudioContext; - if (!AudioCtor) return; - const audio = new AudioCtor(); - const gain = audio.createGain(); - gain.gain.value = 0.04; - gain.connect(audio.destination); - [0, 0.12].forEach((offset, index) => { - const osc = audio.createOscillator(); - osc.type = "sine"; - osc.frequency.value = index ? 980 : 760; - osc.connect(gain); - osc.start(audio.currentTime + offset); - osc.stop(audio.currentTime + offset + 0.09); - }); - setTimeout(() => audio.close().catch(() => {}), 420); + return { infoButton }; } async function loadExistingConversation(messages, panel) { @@ -333,7 +291,6 @@ if (options.notify !== false && hasNewAdminMessage) { messageEffectState.unreadAdminCount += 1; applyAdminReplyEffects(launcher, settings, messageEffectState.unreadAdminCount); - playNotifySound(); } } @@ -566,11 +523,8 @@ header strong { font-size: 16px; } header small { opacity: .92; font-size: 13px; line-height: 1.35; } .mf-header-actions { position: absolute; top: 9px; right: 9px; display: flex; gap: 6px; } - .mf-close, .mf-sound, .mf-info { width: 30px; height: 30px; display: grid; place-items: center; border: 0; border-radius: 6px; background: rgb(255 255 255 / 16%); color: ${c.brandText}; cursor: pointer; padding: 0; } - .mf-close svg, .mf-sound svg, .mf-info svg { width: 19px; height: 19px; fill: currentColor; } - .mf-sound .mf-sound-off { display: none; } - .mf-sound.muted .mf-sound-on { display: none; } - .mf-sound.muted .mf-sound-off { display: block; } + .mf-close, .mf-info { width: 30px; height: 30px; display: grid; place-items: center; border: 0; border-radius: 6px; background: rgb(255 255 255 / 16%); color: ${c.brandText}; cursor: pointer; padding: 0; } + .mf-close svg, .mf-info svg { width: 19px; height: 19px; fill: currentColor; } .mf-messages { padding: 14px; overflow: auto; display: flex; flex-direction: column; gap: 10px; } .mf-message { max-width: 86%; padding: 9px 11px; border-radius: 8px; background: #f1f5f2; color: ${c.text}; } .mf-message.admin { align-self: flex-start; background: #eef7f4; } @@ -637,9 +591,9 @@ function loadSession() { try { const existing = JSON.parse(localStorage.getItem(storageKey) || "{}"); - return { visitorToken: existing.visitorToken || randomId(), conversationId: existing.conversationId || null, history: existing.history || [], soundMuted: Boolean(existing.soundMuted) }; + return { visitorToken: existing.visitorToken || randomId(), conversationId: existing.conversationId || null, history: existing.history || [] }; } catch { - return { visitorToken: randomId(), conversationId: null, history: [], soundMuted: false }; + return { visitorToken: randomId(), conversationId: null, history: [] }; } } @@ -647,8 +601,7 @@ localStorage.setItem(storageKey, JSON.stringify({ visitorToken: session.visitorToken, conversationId: session.conversationId, - history: session.history || [], - soundMuted: Boolean(session.soundMuted) + history: session.history || [] })); }