Move notification sound to admin
This commit is contained in:
+26
-6
@@ -78,15 +78,15 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; }
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 10px;
|
gap: 6px;
|
||||||
padding: 14px;
|
padding: 10px;
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
.brand h1 { margin: 0; font-size: 20px; }
|
.brand h1 { margin: 0; font-size: 18px; }
|
||||||
.brand-actions {
|
.brand-actions {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 5px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
.brand button { background: #edf3ef; color: var(--ink); }
|
.brand button { background: #edf3ef; color: var(--ink); }
|
||||||
@@ -102,10 +102,30 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; }
|
|||||||
height: 18px;
|
height: 18px;
|
||||||
fill: currentColor;
|
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 {
|
.logout-compact {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0 10px;
|
padding: 0 8px;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
.online-dot-toggle {
|
.online-dot-toggle {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
|
|||||||
@@ -33,6 +33,17 @@
|
|||||||
<path d="M19.4 13.5c.1-.5.1-1 .1-1.5s0-1-.1-1.5l2-1.5-2-3.5-2.4 1a8 8 0 0 0-2.6-1.5L14 2h-4l-.4 2.5A8 8 0 0 0 7 6L4.6 5l-2 3.5 2 1.5a9.7 9.7 0 0 0 0 3l-2 1.5 2 3.5L7 18a8 8 0 0 0 2.6 1.5L10 22h4l.4-2.5A8 8 0 0 0 17 18l2.4 1 2-3.5-2-1.5ZM12 15.5a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7Z"></path>
|
<path d="M19.4 13.5c.1-.5.1-1 .1-1.5s0-1-.1-1.5l2-1.5-2-3.5-2.4 1a8 8 0 0 0-2.6-1.5L14 2h-4l-.4 2.5A8 8 0 0 0 7 6L4.6 5l-2 3.5 2 1.5a9.7 9.7 0 0 0 0 3l-2 1.5 2 3.5L7 18a8 8 0 0 0 2.6 1.5L10 22h4l.4-2.5A8 8 0 0 0 17 18l2.4 1 2-3.5-2-1.5ZM12 15.5a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7Z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<div class="admin-sound-control" title="Zvuk notifikaci v BO">
|
||||||
|
<button id="adminSoundToggle" class="icon-button" type="button" aria-label="Vypnout zvuk notifikaci">
|
||||||
|
<svg class="sound-on" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M4 9v6h4l5 4V5L8 9H4Zm12.2-1.8-1.4 1.4A4.9 4.9 0 0 1 16 12c0 1.3-.5 2.5-1.2 3.4l1.4 1.4A6.9 6.9 0 0 0 18 12c0-1.8-.7-3.5-1.8-4.8Zm2.6-2.6-1.4 1.4A8.8 8.8 0 0 1 20 12a8.8 8.8 0 0 1-2.6 6l1.4 1.4A10.8 10.8 0 0 0 22 12c0-2.9-1.2-5.5-3.2-7.4Z"></path>
|
||||||
|
</svg>
|
||||||
|
<svg class="sound-off" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M4 9v6h4l5 4V5L8 9H4Zm12.4 3 2.3-2.3-1.4-1.4-2.3 2.3-2.3-2.3-1.4 1.4 2.3 2.3-2.3 2.3 1.4 1.4 2.3-2.3 2.3 2.3 1.4-1.4-2.3-2.3Z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<input id="adminSoundVolume" type="range" min="0" max="100" step="5" aria-label="Hlasitost notifikaci">
|
||||||
|
</div>
|
||||||
<button id="logoutButton" class="logout-compact" type="button">Odhlasit</button>
|
<button id="logoutButton" class="logout-compact" type="button">Odhlasit</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+37
-1
@@ -9,6 +9,8 @@ const state = {
|
|||||||
events: null,
|
events: null,
|
||||||
statusFilter: "active",
|
statusFilter: "active",
|
||||||
hideBots: localStorage.getItem("mf_admin_hide_bots") !== "0",
|
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: [],
|
replyFiles: [],
|
||||||
attentionTimer: null,
|
attentionTimer: null,
|
||||||
attentionOn: false,
|
attentionOn: false,
|
||||||
@@ -19,6 +21,7 @@ const state = {
|
|||||||
const $ = (selector) => document.querySelector(selector);
|
const $ = (selector) => document.querySelector(selector);
|
||||||
|
|
||||||
boot();
|
boot();
|
||||||
|
renderAdminSoundControls();
|
||||||
|
|
||||||
async function boot() {
|
async function boot() {
|
||||||
const me = await api("/api/admin/me").catch(() => null);
|
const me = await api("/api/admin/me").catch(() => null);
|
||||||
@@ -47,6 +50,20 @@ $("#logoutButton").addEventListener("click", async () => {
|
|||||||
|
|
||||||
$("#settingsButton").addEventListener("click", () => $("#settingsPanel").classList.remove("hidden"));
|
$("#settingsButton").addEventListener("click", () => $("#settingsPanel").classList.remove("hidden"));
|
||||||
$("#closeSettingsButton").addEventListener("click", () => $("#settingsPanel").classList.add("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").brand.addEventListener("input", (event) => {
|
||||||
$("#settingsForm").brandHex.value = normalizeHexColor(event.target.value) || event.target.value;
|
$("#settingsForm").brandHex.value = normalizeHexColor(event.target.value) || event.target.value;
|
||||||
});
|
});
|
||||||
@@ -775,12 +792,26 @@ function notify() {
|
|||||||
playNotifySound();
|
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() {
|
function playNotifySound() {
|
||||||
|
const volume = clamp(state.soundVolume, 0, 100);
|
||||||
|
if (state.soundMuted || volume <= 0) return;
|
||||||
const AudioCtor = window.AudioContext || window.webkitAudioContext;
|
const AudioCtor = window.AudioContext || window.webkitAudioContext;
|
||||||
if (!AudioCtor) return;
|
if (!AudioCtor) return;
|
||||||
const audio = new AudioCtor();
|
const audio = new AudioCtor();
|
||||||
const gain = audio.createGain();
|
const gain = audio.createGain();
|
||||||
gain.gain.value = 0.055;
|
gain.gain.value = 0.12 * (volume / 100);
|
||||||
gain.connect(audio.destination);
|
gain.connect(audio.destination);
|
||||||
[0, 0.14].forEach((offset, index) => {
|
[0, 0.14].forEach((offset, index) => {
|
||||||
const osc = audio.createOscillator();
|
const osc = audio.createOscillator();
|
||||||
@@ -793,6 +824,11 @@ function playNotifySound() {
|
|||||||
setTimeout(() => audio.close().catch(() => {}), 520);
|
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() {
|
function startAttention() {
|
||||||
if (state.attentionTimer) return;
|
if (state.attentionTimer) return;
|
||||||
const favicon = document.querySelector("link[rel='icon']");
|
const favicon = document.querySelector("link[rel='icon']");
|
||||||
|
|||||||
+8
-55
@@ -66,7 +66,7 @@
|
|||||||
const panel = root.querySelector(".mf-panel");
|
const panel = root.querySelector(".mf-panel");
|
||||||
const launcher = root.querySelector(".mf-launcher");
|
const launcher = root.querySelector(".mf-launcher");
|
||||||
const close = root.querySelector(".mf-close");
|
const close = root.querySelector(".mf-close");
|
||||||
const { soundButton, infoButton } = createHeaderActions(root, close);
|
const { infoButton } = createHeaderActions(root, close);
|
||||||
const form = root.querySelector(".mf-form");
|
const form = root.querySelector(".mf-form");
|
||||||
const messages = root.querySelector(".mf-messages");
|
const messages = root.querySelector(".mf-messages");
|
||||||
const typing = root.querySelector(".mf-typing");
|
const typing = root.querySelector(".mf-typing");
|
||||||
@@ -77,7 +77,6 @@
|
|||||||
unreadAdminCount: 0,
|
unreadAdminCount: 0,
|
||||||
knownAdminMessageId: 0
|
knownAdminMessageId: 0
|
||||||
};
|
};
|
||||||
renderSoundState(soundButton);
|
|
||||||
|
|
||||||
launcher.addEventListener("click", () => {
|
launcher.addEventListener("click", () => {
|
||||||
panel.classList.toggle("open");
|
panel.classList.toggle("open");
|
||||||
@@ -91,11 +90,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
close.addEventListener("click", () => panel.classList.remove("open"));
|
close.addEventListener("click", () => panel.classList.remove("open"));
|
||||||
soundButton.addEventListener("click", () => {
|
|
||||||
session.soundMuted = !session.soundMuted;
|
|
||||||
saveSession();
|
|
||||||
renderSoundState(soundButton);
|
|
||||||
});
|
|
||||||
infoButton.addEventListener("click", () => {
|
infoButton.addEventListener("click", () => {
|
||||||
root.querySelector(".mf-gdpr").hidden = !root.querySelector(".mf-gdpr").hidden;
|
root.querySelector(".mf-gdpr").hidden = !root.querySelector(".mf-gdpr").hidden;
|
||||||
});
|
});
|
||||||
@@ -234,17 +228,6 @@
|
|||||||
function createHeaderActions(root, closeButton) {
|
function createHeaderActions(root, closeButton) {
|
||||||
const actions = document.createElement("div");
|
const actions = document.createElement("div");
|
||||||
actions.className = "mf-header-actions";
|
actions.className = "mf-header-actions";
|
||||||
const soundButton = document.createElement("button");
|
|
||||||
soundButton.className = "mf-sound";
|
|
||||||
soundButton.type = "button";
|
|
||||||
soundButton.innerHTML = `
|
|
||||||
<svg class="mf-sound-on" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path d="M4 9v6h4l5 4V5L8 9H4Zm12.2-1.8-1.4 1.4A4.9 4.9 0 0 1 16 12c0 1.3-.5 2.5-1.2 3.4l1.4 1.4A6.9 6.9 0 0 0 18 12c0-1.8-.7-3.5-1.8-4.8Zm2.6-2.6-1.4 1.4A8.8 8.8 0 0 1 20 12a8.8 8.8 0 0 1-2.6 6l1.4 1.4A10.8 10.8 0 0 0 22 12c0-2.9-1.2-5.5-3.2-7.4Z"></path>
|
|
||||||
</svg>
|
|
||||||
<svg class="mf-sound-off" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path d="M4 9v6h4l5 4V5L8 9H4Zm12.4 3 2.3-2.3-1.4-1.4-2.3 2.3-2.3-2.3-1.4 1.4 2.3 2.3-2.3 2.3 1.4 1.4 2.3-2.3 2.3 2.3 1.4-1.4-2.3-2.3Z"></path>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
const infoButton = document.createElement("button");
|
const infoButton = document.createElement("button");
|
||||||
infoButton.className = "mf-info";
|
infoButton.className = "mf-info";
|
||||||
infoButton.type = "button";
|
infoButton.type = "button";
|
||||||
@@ -255,38 +238,13 @@
|
|||||||
closeButton.setAttribute("aria-label", "Zmensit chat");
|
closeButton.setAttribute("aria-label", "Zmensit chat");
|
||||||
closeButton.title = "Zmensit chat";
|
closeButton.title = "Zmensit chat";
|
||||||
closeButton.before(actions);
|
closeButton.before(actions);
|
||||||
actions.append(soundButton, infoButton, closeButton);
|
actions.append(infoButton, closeButton);
|
||||||
const gdpr = document.createElement("p");
|
const gdpr = document.createElement("p");
|
||||||
gdpr.className = "mf-gdpr";
|
gdpr.className = "mf-gdpr";
|
||||||
gdpr.hidden = true;
|
gdpr.hidden = true;
|
||||||
gdpr.textContent = "Odeslanim zpravy nam predavate udaje potrebne pro odpoved. Historii chatu uchovavame pro zakaznickou podporu.";
|
gdpr.textContent = "Odeslanim zpravy nam predavate udaje potrebne pro odpoved. Historii chatu uchovavame pro zakaznickou podporu.";
|
||||||
root.querySelector(".mf-form").prepend(gdpr);
|
root.querySelector(".mf-form").prepend(gdpr);
|
||||||
return { soundButton, infoButton };
|
return { 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadExistingConversation(messages, panel) {
|
async function loadExistingConversation(messages, panel) {
|
||||||
@@ -333,7 +291,6 @@
|
|||||||
if (options.notify !== false && hasNewAdminMessage) {
|
if (options.notify !== false && hasNewAdminMessage) {
|
||||||
messageEffectState.unreadAdminCount += 1;
|
messageEffectState.unreadAdminCount += 1;
|
||||||
applyAdminReplyEffects(launcher, settings, messageEffectState.unreadAdminCount);
|
applyAdminReplyEffects(launcher, settings, messageEffectState.unreadAdminCount);
|
||||||
playNotifySound();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,11 +523,8 @@
|
|||||||
header strong { font-size: 16px; }
|
header strong { font-size: 16px; }
|
||||||
header small { opacity: .92; font-size: 13px; line-height: 1.35; }
|
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-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, .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-close 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-messages { padding: 14px; overflow: auto; display: flex; flex-direction: column; gap: 10px; }
|
.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 { max-width: 86%; padding: 9px 11px; border-radius: 8px; background: #f1f5f2; color: ${c.text}; }
|
||||||
.mf-message.admin { align-self: flex-start; background: #eef7f4; }
|
.mf-message.admin { align-self: flex-start; background: #eef7f4; }
|
||||||
@@ -637,9 +591,9 @@
|
|||||||
function loadSession() {
|
function loadSession() {
|
||||||
try {
|
try {
|
||||||
const existing = JSON.parse(localStorage.getItem(storageKey) || "{}");
|
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 {
|
} catch {
|
||||||
return { visitorToken: randomId(), conversationId: null, history: [], soundMuted: false };
|
return { visitorToken: randomId(), conversationId: null, history: [] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,8 +601,7 @@
|
|||||||
localStorage.setItem(storageKey, JSON.stringify({
|
localStorage.setItem(storageKey, JSON.stringify({
|
||||||
visitorToken: session.visitorToken,
|
visitorToken: session.visitorToken,
|
||||||
conversationId: session.conversationId,
|
conversationId: session.conversationId,
|
||||||
history: session.history || [],
|
history: session.history || []
|
||||||
soundMuted: Boolean(session.soundMuted)
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user