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 || []
}));
}