diff --git a/public/admin.html b/public/admin.html index b853151..8ef2b17 100644 --- a/public/admin.html +++ b/public/admin.html @@ -85,6 +85,10 @@ +

GTM snippet

diff --git a/public/admin.js b/public/admin.js index 4e229d0..f2fc8e1 100644 --- a/public/admin.js +++ b/public/admin.js @@ -60,6 +60,8 @@ $("#settingsForm").addEventListener("submit", async (event) => { settings.mobile.side = form.get("mobileSide"); settings.mobile.bottomPx = Number(form.get("mobileBottom")); settings.mobile.sideOffsetPx = Number(form.get("mobileOffset")); + settings.notifications = settings.notifications || {}; + settings.notifications.pulseOnAdminReply = form.get("pulseOnAdminReply") === "on"; await saveSite({ settings, isOnline: state.site.isOnline }); }); @@ -76,6 +78,7 @@ $("#replyForm").addEventListener("submit", async (event) => { method: "POST", body: payload }); + event.currentTarget.querySelector("textarea[name='message']").value = ""; event.currentTarget.reset(); state.replyFiles = []; renderReplyAttachments(); @@ -243,6 +246,7 @@ function renderSite() { form.mobileSide.value = s.mobile.side || "right"; form.mobileBottom.value = s.mobile.bottomPx || 14; form.mobileOffset.value = s.mobile.sideOffsetPx || 12; + form.pulseOnAdminReply.checked = s.notifications?.pulseOnAdminReply !== false; } function renderConversations() { diff --git a/public/widget.js b/public/widget.js index ddff0ad..076d514 100644 --- a/public/widget.js +++ b/public/widget.js @@ -68,7 +68,10 @@ const attachmentList = root.querySelector(".mf-attachment-list"); let selectedFiles = []; - launcher.addEventListener("click", () => panel.classList.toggle("open")); + launcher.addEventListener("click", () => { + panel.classList.toggle("open"); + if (panel.classList.contains("open")) launcher.classList.remove("mf-notify"); + }); close.addEventListener("click", () => panel.classList.remove("open")); form.message.addEventListener("input", debounce(() => { @@ -133,7 +136,7 @@ selectedFiles = []; renderSelectedFiles(attachmentList, selectedFiles); renderConversation(messages, data); - connectEvents(messages, typing); + connectEvents(messages, typing, panel, launcher, settings); } catch { showFormError(root, "Zpravu se nepodarilo odeslat. Zkuste to prosim znovu."); } finally { @@ -143,7 +146,7 @@ rememberPage(); if (session.conversationId) { - loadExistingConversation(messages).finally(() => connectEvents(messages, typing)); + loadExistingConversation(messages).finally(() => connectEvents(messages, typing, panel, launcher, settings)); } } @@ -155,7 +158,7 @@ }; } - function connectEvents(messages, typing) { + function connectEvents(messages, typing, panel, launcher, settings) { if (session.events || !session.conversationId) return; session.events = new EventSource(`${apiBase}/api/widget/conversations/${session.conversationId}/events`); session.events.addEventListener("message", (event) => { @@ -165,7 +168,14 @@ clearTimeout(connectEvents.typingTimer); connectEvents.typingTimer = setTimeout(() => { typing.hidden = true; }, 1800); } - if (data.type === "message:new") renderConversation(messages, data.payload); + if (data.type === "message:new") { + renderConversation(messages, data.payload); + const eventMessages = data.payload?.messages || []; + const lastMessage = eventMessages[eventMessages.length - 1]; + if (settings.notifications?.pulseOnAdminReply !== false && lastMessage?.senderType === "admin" && !panel.classList.contains("open")) { + launcher.classList.add("mf-notify"); + } + } }); } @@ -276,6 +286,11 @@ .mf-launcher span { width: 26px; margin: 0 auto; } .mf-launcher span:before { transform: translateY(-8px); } .mf-launcher span:after { transform: translateY(5px); width: 18px; } + .mf-launcher.mf-notify { animation: mfPulse 1.15s ease-out infinite; } + .mf-launcher.mf-notify:after { + position: absolute; top: 5px; right: 5px; width: 13px; height: 13px; border-radius: 50%; + background: #ff3b30; border: 2px solid ${c.surface}; content: ""; + } .mf-panel { position: fixed; z-index: 2147483001; ${desktop.side}: ${desktop.sideOffsetPx}px; bottom: ${desktop.bottomPx + 72}px; width: min(${desktop.widthPx}px, calc(100vw - 24px)); max-height: min(680px, calc(100vh - 110px)); display: none; @@ -313,6 +328,11 @@ .mf-note.error { color: #b42318; } .mf-powered { justify-self: center; color: ${c.muted}; font-size: 11px; text-decoration: none; } .mf-powered:hover { color: ${c.brand}; text-decoration: underline; } + @keyframes mfPulse { + 0% { box-shadow: 0 12px 30px rgb(0 0 0 / 22%), 0 0 0 0 rgb(15 143 111 / 42%); transform: scale(1); } + 70% { box-shadow: 0 12px 30px rgb(0 0 0 / 22%), 0 0 0 14px rgb(15 143 111 / 0%); transform: scale(1.04); } + 100% { box-shadow: 0 12px 30px rgb(0 0 0 / 22%), 0 0 0 0 rgb(15 143 111 / 0%); transform: scale(1); } + } @media (max-width: 640px) { .mf-launcher { ${mobile.side}: ${mobile.sideOffsetPx}px; bottom: ${mobile.bottomPx}px; } .mf-panel { ${mobile.side}: ${mobile.sideOffsetPx}px; bottom: ${mobile.bottomPx + 68}px; width: min(${mobile.widthPx}px, calc(100vw - 24px)); max-height: calc(100vh - 96px); } diff --git a/server.js b/server.js index 66623d6..416fec5 100644 --- a/server.js +++ b/server.js @@ -219,7 +219,8 @@ function defaultSettings() { muted: "#647067" }, desktop: { side: "right", bottomPx: 22, sideOffsetPx: 22, widthPx: 370 }, - mobile: { side: "right", bottomPx: 14, sideOffsetPx: 12, widthPx: 340 } + mobile: { side: "right", bottomPx: 14, sideOffsetPx: 12, widthPx: 340 }, + notifications: { pulseOnAdminReply: true } }; } @@ -614,13 +615,15 @@ function formatAttachment(row) { } function formatSite(site) { + const settings = parseJson(site.settings_json, defaultSettings()); + settings.notifications = { ...defaultSettings().notifications, ...(settings.notifications || {}) }; return { id: site.id, siteKey: site.site_key, name: site.name, domain: site.domain, isOnline: Boolean(site.is_online), - settings: parseJson(site.settings_json, defaultSettings()) + settings }; }