From e1d63db62fdcf0346c37616fae69d9cb19967da3 Mon Sep 17 00:00:00 2001 From: rajchales-monito Date: Fri, 24 Jul 2026 10:28:48 +0200 Subject: [PATCH] Add widget sound toggle and enter send --- public/widget.js | 77 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/public/widget.js b/public/widget.js index 61d5e8d..8b92273 100644 --- a/public/widget.js +++ b/public/widget.js @@ -61,24 +61,37 @@ const panel = root.querySelector(".mf-panel"); const launcher = root.querySelector(".mf-launcher"); const close = root.querySelector(".mf-close"); + const soundButton = createSoundButton(root, close); const form = root.querySelector(".mf-form"); const messages = root.querySelector(".mf-messages"); const typing = root.querySelector(".mf-typing"); const dropzone = root.querySelector(".mf-dropzone"); const attachmentList = root.querySelector(".mf-attachment-list"); let selectedFiles = []; + renderSoundState(soundButton); launcher.addEventListener("click", () => { panel.classList.toggle("open"); if (panel.classList.contains("open")) launcher.classList.remove("mf-notify"); }); close.addEventListener("click", () => panel.classList.remove("open")); + soundButton.addEventListener("click", () => { + session.soundMuted = !session.soundMuted; + saveSession(); + renderSoundState(soundButton); + }); form.message.addEventListener("input", debounce(() => { if (!session.conversationId) return; fetch(`${apiBase}/api/widget/conversations/${session.conversationId}/typing`, { method: "POST" }).catch(() => {}); }, 700)); + form.message.addEventListener("keydown", (event) => { + if (event.key !== "Enter" || event.shiftKey) return; + event.preventDefault(); + form.requestSubmit(); + }); + form.attachments.addEventListener("change", () => { selectedFiles = mergeImageFiles(selectedFiles, [...form.attachments.files]); renderSelectedFiles(attachmentList, selectedFiles); @@ -174,11 +187,59 @@ const lastMessage = eventMessages[eventMessages.length - 1]; if (settings.notifications?.pulseOnAdminReply !== false && lastMessage?.senderType === "admin" && !panel.classList.contains("open")) { launcher.classList.add("mf-notify"); + playNotifySound(); } } }); } + function createSoundButton(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 = ` + + + `; + closeButton.innerHTML = ``; + closeButton.setAttribute("aria-label", "Zmensit chat"); + closeButton.title = "Zmensit chat"; + closeButton.before(actions); + actions.append(soundButton, closeButton); + return soundButton; + } + + 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) { try { const data = await fetchJson(`${apiBase}/api/widget/conversations/${session.conversationId}?visitorToken=${encodeURIComponent(session.visitorToken)}`); @@ -293,10 +354,15 @@ background: ${c.surface}; border: 1px solid #dfe6e2; border-radius: 8px; overflow: hidden; box-shadow: 0 18px 50px rgb(0 0 0 / 18%); } .mf-panel.open { display: grid; grid-template-rows: auto minmax(130px, 1fr) auto auto; } - header { position: relative; display: grid; gap: 3px; background: ${c.brand}; color: ${c.brandText}; padding: 15px 46px 15px 16px; } + header { position: relative; display: grid; gap: 3px; background: ${c.brand}; color: ${c.brandText}; padding: 15px 86px 15px 16px; } header strong { font-size: 16px; } header small { opacity: .92; font-size: 13px; line-height: 1.35; } - .mf-close { position: absolute; top: 9px; right: 9px; width: 30px; height: 30px; border: 0; border-radius: 6px; background: rgb(255 255 255 / 16%); color: ${c.brandText}; cursor: pointer; } + .mf-header-actions { position: absolute; top: 9px; right: 9px; display: flex; gap: 6px; } + .mf-close, .mf-sound { 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 { 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-message { max-width: 86%; padding: 9px 11px; border-radius: 8px; background: #f1f5f2; color: ${c.text}; } .mf-message.admin { align-self: flex-start; background: #eef7f4; } @@ -346,9 +412,9 @@ function loadSession() { try { const existing = JSON.parse(localStorage.getItem(storageKey) || "{}"); - return { visitorToken: existing.visitorToken || randomId(), conversationId: existing.conversationId || null, history: existing.history || [] }; + return { visitorToken: existing.visitorToken || randomId(), conversationId: existing.conversationId || null, history: existing.history || [], soundMuted: Boolean(existing.soundMuted) }; } catch { - return { visitorToken: randomId(), conversationId: null, history: [] }; + return { visitorToken: randomId(), conversationId: null, history: [], soundMuted: false }; } } @@ -356,7 +422,8 @@ localStorage.setItem(storageKey, JSON.stringify({ visitorToken: session.visitorToken, conversationId: session.conversationId, - history: session.history || [] + history: session.history || [], + soundMuted: Boolean(session.soundMuted) })); }