diff --git a/public/admin.html b/public/admin.html index 51cb41d..f22d2aa 100644 --- a/public/admin.html +++ b/public/admin.html @@ -115,13 +115,15 @@ - - +
+ Efekt pri nove zprave + + + + +

GTM snippet

diff --git a/public/admin.js b/public/admin.js index 2090c63..6318423 100644 --- a/public/admin.js +++ b/public/admin.js @@ -83,11 +83,13 @@ $("#settingsForm").addEventListener("submit", async (event) => { breathe: form.get("launcherBreathe") === "on", ring: form.get("launcherRing") === "on", hoverLabel: form.get("launcherHoverLabel") === "on", - wave: form.get("launcherWave") === "on", avatar: form.get("launcherAvatar") === "on" }; settings.notifications = settings.notifications || {}; - settings.notifications.pulseOnAdminReply = form.get("pulseOnAdminReply") === "on"; + settings.notifications.pulseOnAdminReply = form.get("messagePulse") === "on"; + settings.notifications.badgeOnAdminReply = form.get("messageBadge") === "on"; + settings.notifications.labelOnAdminReply = form.get("messageLabel") === "on"; + settings.notifications.wiggleOnAdminReply = form.get("messageWiggle") === "on"; await saveSite({ settings, isOnline: state.site.isOnline }); $("#settingsPanel").classList.add("hidden"); }); @@ -334,9 +336,11 @@ function renderSite() { form.launcherBreathe.checked = s.launcherEffects?.breathe !== false; form.launcherRing.checked = s.launcherEffects?.ring !== false; form.launcherHoverLabel.checked = s.launcherEffects?.hoverLabel !== false; - form.launcherWave.checked = s.launcherEffects?.wave !== false; form.launcherAvatar.checked = s.launcherEffects?.avatar !== false; - form.pulseOnAdminReply.checked = s.notifications?.pulseOnAdminReply !== false; + form.messagePulse.checked = s.notifications?.pulseOnAdminReply !== false; + form.messageBadge.checked = s.notifications?.badgeOnAdminReply !== false; + form.messageLabel.checked = s.notifications?.labelOnAdminReply !== false; + form.messageWiggle.checked = s.notifications?.wiggleOnAdminReply !== false; } function renderConversations() { diff --git a/public/widget.js b/public/widget.js index 54ec460..c05ab6f 100644 --- a/public/widget.js +++ b/public/widget.js @@ -28,6 +28,8 @@
@@ -70,12 +72,16 @@ const dropzone = root.querySelector(".mf-dropzone"); const attachmentList = root.querySelector(".mf-attachment-list"); let selectedFiles = []; + let unreadAdminCount = 0; renderSoundState(soundButton); launcher.addEventListener("click", () => { panel.classList.toggle("open"); if (panel.classList.contains("open")) { launcher.classList.remove("mf-notify"); + clearMessageEffects(launcher); + unreadAdminCount = 0; + updateMessageBadge(launcher, unreadAdminCount); scrollMessagesToBottom(messages); markConversationSeen(); } @@ -204,8 +210,9 @@ if (panel.classList.contains("open")) markConversationSeen(); 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"); + if (lastMessage?.senderType === "admin" && !panel.classList.contains("open")) { + unreadAdminCount += 1; + applyAdminReplyEffects(launcher, settings, unreadAdminCount); playNotifySound(); } } @@ -421,11 +428,41 @@ effects.breathe ? "mf-effect-breathe" : "", effects.ring ? "mf-effect-ring" : "", effects.hoverLabel ? "mf-effect-hover-label" : "", - effects.wave ? "mf-effect-wave" : "", effects.avatar ? "mf-effect-avatar" : "" ].filter(Boolean).join(" "); } + function applyAdminReplyEffects(launcher, settings, unreadCount) { + const notifications = settings.notifications || {}; + if (notifications.pulseOnAdminReply !== false) launcher.classList.add("mf-notify"); + if (notifications.badgeOnAdminReply !== false) updateMessageBadge(launcher, unreadCount); + if (notifications.labelOnAdminReply !== false) { + launcher.classList.add("mf-show-message-label"); + clearTimeout(applyAdminReplyEffects.labelTimer); + applyAdminReplyEffects.labelTimer = setTimeout(() => launcher.classList.remove("mf-show-message-label"), 4200); + } + if (notifications.wiggleOnAdminReply !== false) { + launcher.classList.remove("mf-wiggle"); + void launcher.offsetWidth; + launcher.classList.add("mf-wiggle"); + clearTimeout(applyAdminReplyEffects.wiggleTimer); + applyAdminReplyEffects.wiggleTimer = setTimeout(() => launcher.classList.remove("mf-wiggle"), 1400); + } + } + + function clearMessageEffects(launcher) { + launcher.classList.remove("mf-notify", "mf-wiggle", "mf-show-message-label"); + clearTimeout(applyAdminReplyEffects.labelTimer); + clearTimeout(applyAdminReplyEffects.wiggleTimer); + } + + function updateMessageBadge(launcher, unreadCount) { + const badge = launcher.querySelector(".mf-message-badge"); + if (!badge) return; + badge.hidden = unreadCount <= 0; + badge.textContent = unreadCount > 9 ? "9+" : String(unreadCount); + } + function styles(settings) { const c = settings.colors; const desktop = settings.desktop; @@ -446,14 +483,17 @@ .mf-launcher-icon:after { transform: translateY(5px); width: 18px; } .mf-launcher-label { position: absolute; ${labelOffset} top: 50%; transform: translateY(-50%) translateX(8px); opacity: 0; pointer-events: none; white-space: nowrap; border-radius: 999px; padding: 7px 10px; background: ${c.text}; color: white; font-size: 12px; box-shadow: 0 10px 24px rgb(0 0 0 / 18%); transition: opacity .18s ease, transform .18s ease; } .mf-effect-hover-label:hover .mf-launcher-label { opacity: 1; transform: translateY(-50%) translateX(0); } + .mf-message-label { position: absolute; ${labelOffset} top: 50%; transform: translateY(-50%) translateX(8px); opacity: 0; pointer-events: none; white-space: nowrap; border-radius: 999px; padding: 7px 10px; background: ${c.text}; color: white; font-size: 12px; font-weight: 700; box-shadow: 0 10px 24px rgb(0 0 0 / 18%); transition: opacity .18s ease, transform .18s ease; } + .mf-show-message-label .mf-message-label { opacity: 1; transform: translateY(-50%) translateX(0); } + .mf-message-badge { position: absolute; top: -5px; right: -5px; min-width: 22px; height: 22px; display: grid; place-items: center; border-radius: 999px; border: 2px solid white; background: #ff3b30; color: white; font-size: 12px; font-weight: 800; line-height: 1; box-shadow: 0 8px 18px rgb(0 0 0 / 18%); } .mf-online.mf-effect-breathe { animation: mfBreathe 2.8s ease-in-out infinite; } .mf-online.mf-effect-ring:before { position: absolute; inset: -5px; border: 2px solid ${c.brand}; border-radius: 50%; opacity: .62; content: ""; animation: mfRing 2.2s ease-out infinite; } - .mf-online.mf-effect-wave:after { position: absolute; inset: 0; border-radius: 50%; content: ""; box-shadow: 0 0 0 0 rgb(15 143 111 / 42%); animation: mfWaveOnce 1.7s ease-out 1 .6s; pointer-events: none; } .mf-effect-avatar .mf-launcher-icon { width: 26px; height: 20px; border-radius: 12px 12px 10px 10px; background: rgb(255 255 255 / 24%); border: 2px solid ${c.brandText}; position: relative; } .mf-effect-avatar .mf-launcher-icon:before { position: absolute; left: 6px; top: 6px; width: 4px; height: 4px; border-radius: 50%; background: ${c.brandText}; transform: none; box-shadow: 10px 0 0 ${c.brandText}; } .mf-effect-avatar .mf-launcher-icon:after { position: absolute; left: 7px; top: 13px; width: 12px; height: 2px; border-radius: 999px; background: ${c.brandText}; transform: none; opacity: .88; } .mf-offline { filter: grayscale(.2) saturate(.72); opacity: .86; } .mf-launcher.mf-notify { animation: mfPulse 1.15s ease-out infinite; } + .mf-launcher.mf-wiggle { animation: mfWiggle .62s ease-in-out 2; } .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; @@ -512,9 +552,11 @@ 70% { transform: scale(1.22); opacity: 0; } 100% { transform: scale(.92); opacity: 0; } } - @keyframes mfWaveOnce { - 0% { transform: scale(1); opacity: .7; } - 100% { transform: scale(1.58); opacity: 0; } + @keyframes mfWiggle { + 0%, 100% { transform: translateY(0) rotate(0deg); } + 22% { transform: translateY(-3px) rotate(-4deg); } + 48% { transform: translateY(2px) rotate(3deg); } + 72% { transform: translateY(-1px) rotate(-2deg); } } @media (max-width: 640px) { .mf-launcher { ${mobile.side}: ${mobile.sideOffsetPx}px; bottom: ${mobile.bottomPx}px; } diff --git a/server.js b/server.js index ef63368..b3b22f7 100644 --- a/server.js +++ b/server.js @@ -267,10 +267,14 @@ function defaultSettings() { breathe: true, ring: true, hoverLabel: true, - wave: true, avatar: true }, - notifications: { pulseOnAdminReply: true } + notifications: { + pulseOnAdminReply: true, + badgeOnAdminReply: true, + labelOnAdminReply: true, + wiggleOnAdminReply: true + } }; }