Add widget sound toggle and enter send
This commit is contained in:
+72
-5
@@ -61,24 +61,37 @@
|
|||||||
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 = createSoundButton(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");
|
||||||
const dropzone = root.querySelector(".mf-dropzone");
|
const dropzone = root.querySelector(".mf-dropzone");
|
||||||
const attachmentList = root.querySelector(".mf-attachment-list");
|
const attachmentList = root.querySelector(".mf-attachment-list");
|
||||||
let selectedFiles = [];
|
let selectedFiles = [];
|
||||||
|
renderSoundState(soundButton);
|
||||||
|
|
||||||
launcher.addEventListener("click", () => {
|
launcher.addEventListener("click", () => {
|
||||||
panel.classList.toggle("open");
|
panel.classList.toggle("open");
|
||||||
if (panel.classList.contains("open")) launcher.classList.remove("mf-notify");
|
if (panel.classList.contains("open")) launcher.classList.remove("mf-notify");
|
||||||
});
|
});
|
||||||
close.addEventListener("click", () => panel.classList.remove("open"));
|
close.addEventListener("click", () => panel.classList.remove("open"));
|
||||||
|
soundButton.addEventListener("click", () => {
|
||||||
|
session.soundMuted = !session.soundMuted;
|
||||||
|
saveSession();
|
||||||
|
renderSoundState(soundButton);
|
||||||
|
});
|
||||||
|
|
||||||
form.message.addEventListener("input", debounce(() => {
|
form.message.addEventListener("input", debounce(() => {
|
||||||
if (!session.conversationId) return;
|
if (!session.conversationId) return;
|
||||||
fetch(`${apiBase}/api/widget/conversations/${session.conversationId}/typing`, { method: "POST" }).catch(() => {});
|
fetch(`${apiBase}/api/widget/conversations/${session.conversationId}/typing`, { method: "POST" }).catch(() => {});
|
||||||
}, 700));
|
}, 700));
|
||||||
|
|
||||||
|
form.message.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key !== "Enter" || event.shiftKey) return;
|
||||||
|
event.preventDefault();
|
||||||
|
form.requestSubmit();
|
||||||
|
});
|
||||||
|
|
||||||
form.attachments.addEventListener("change", () => {
|
form.attachments.addEventListener("change", () => {
|
||||||
selectedFiles = mergeImageFiles(selectedFiles, [...form.attachments.files]);
|
selectedFiles = mergeImageFiles(selectedFiles, [...form.attachments.files]);
|
||||||
renderSelectedFiles(attachmentList, selectedFiles);
|
renderSelectedFiles(attachmentList, selectedFiles);
|
||||||
@@ -174,11 +187,59 @@
|
|||||||
const lastMessage = eventMessages[eventMessages.length - 1];
|
const lastMessage = eventMessages[eventMessages.length - 1];
|
||||||
if (settings.notifications?.pulseOnAdminReply !== false && lastMessage?.senderType === "admin" && !panel.classList.contains("open")) {
|
if (settings.notifications?.pulseOnAdminReply !== false && lastMessage?.senderType === "admin" && !panel.classList.contains("open")) {
|
||||||
launcher.classList.add("mf-notify");
|
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 = `
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
closeButton.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6.7 8.7 12 14l5.3-5.3 1.4 1.4L12 16.8l-6.7-6.7 1.4-1.4Z"></path></svg>`;
|
||||||
|
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) {
|
async function loadExistingConversation(messages) {
|
||||||
try {
|
try {
|
||||||
const data = await fetchJson(`${apiBase}/api/widget/conversations/${session.conversationId}?visitorToken=${encodeURIComponent(session.visitorToken)}`);
|
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%);
|
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; }
|
.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 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-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-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; }
|
||||||
@@ -346,9 +412,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 || [] };
|
return { visitorToken: existing.visitorToken || randomId(), conversationId: existing.conversationId || null, history: existing.history || [], soundMuted: Boolean(existing.soundMuted) };
|
||||||
} catch {
|
} catch {
|
||||||
return { visitorToken: randomId(), conversationId: null, history: [] };
|
return { visitorToken: randomId(), conversationId: null, history: [], soundMuted: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,7 +422,8 @@
|
|||||||
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