Fix widget reply notification effects

This commit is contained in:
rajchales-monito
2026-07-24 14:29:03 +02:00
parent a3fc001b40
commit 04529d2df3
2 changed files with 75 additions and 17 deletions
+71 -16
View File
@@ -73,7 +73,10 @@
const dropzone = root.querySelector(".mf-dropzone");
const attachmentList = root.querySelector(".mf-attachment-list");
let selectedFiles = [];
let unreadAdminCount = 0;
const messageEffectState = {
unreadAdminCount: 0,
knownAdminMessageId: 0
};
renderSoundState(soundButton);
launcher.addEventListener("click", () => {
@@ -81,8 +84,8 @@
if (panel.classList.contains("open")) {
launcher.classList.remove("mf-notify");
clearMessageEffects(launcher);
unreadAdminCount = 0;
updateMessageBadge(launcher, unreadAdminCount);
messageEffectState.unreadAdminCount = 0;
updateMessageBadge(launcher, messageEffectState.unreadAdminCount);
scrollMessagesToBottom(messages);
markConversationSeen();
}
@@ -164,8 +167,9 @@
form.message.value = "";
selectedFiles = [];
renderSelectedFiles(attachmentList, selectedFiles);
renderConversation(messages, data);
connectEvents(messages, typing, panel, launcher, settings);
handleConversationUpdate(data, messages, panel, launcher, settings, messageEffectState, { notify: false });
connectEvents(messages, typing, panel, launcher, settings, messageEffectState);
startConversationPolling(messages, panel, launcher, settings, messageEffectState);
} catch {
showFormError(root, "Zpravu se nepodarilo odeslat. Zkuste to prosim znovu.");
} finally {
@@ -184,7 +188,14 @@
}
});
if (session.conversationId) {
loadExistingConversation(messages, panel).finally(() => connectEvents(messages, typing, panel, launcher, settings));
loadExistingConversation(messages, panel)
.then((data) => {
messageEffectState.knownAdminMessageId = latestAdminMessageId(data?.messages || []);
})
.finally(() => {
connectEvents(messages, typing, panel, launcher, settings, messageEffectState);
startConversationPolling(messages, panel, launcher, settings, messageEffectState);
});
}
}
@@ -196,7 +207,7 @@
};
}
function connectEvents(messages, typing, panel, launcher, settings) {
function connectEvents(messages, typing, panel, launcher, settings, messageEffectState) {
if (session.events || !session.conversationId) return;
session.events = new EventSource(`${apiBase}/api/widget/conversations/${session.conversationId}/events`);
session.events.addEventListener("message", (event) => {
@@ -207,17 +218,17 @@
connectEvents.typingTimer = setTimeout(() => { typing.hidden = true; }, 1800);
}
if (data.type === "message:new") {
renderConversation(messages, data.payload);
if (panel.classList.contains("open")) markConversationSeen();
const eventMessages = data.payload?.messages || [];
const lastMessage = eventMessages[eventMessages.length - 1];
if (lastMessage?.senderType === "admin" && !panel.classList.contains("open")) {
unreadAdminCount += 1;
applyAdminReplyEffects(launcher, settings, unreadAdminCount);
playNotifySound();
}
handleConversationUpdate(data.payload, messages, panel, launcher, settings, messageEffectState, { notify: true });
}
});
session.events.onerror = () => {
session.events?.close();
session.events = null;
clearTimeout(connectEvents.reconnectTimer);
connectEvents.reconnectTimer = setTimeout(() => {
connectEvents(messages, typing, panel, launcher, settings, messageEffectState);
}, 2500);
};
}
function createHeaderActions(root, closeButton) {
@@ -283,12 +294,56 @@
const data = await fetchJson(`${apiBase}/api/widget/conversations/${session.conversationId}?visitorToken=${encodeURIComponent(session.visitorToken)}`);
renderConversation(messages, data);
if (panel.classList.contains("open")) markConversationSeen();
return data;
} catch {
session.conversationId = null;
saveSession();
return null;
}
}
function startConversationPolling(messages, panel, launcher, settings, messageEffectState) {
if (session.pollTimer || !session.conversationId) return;
session.pollTimer = setInterval(async () => {
if (!session.conversationId) return;
try {
const data = await fetchJson(`${apiBase}/api/widget/conversations/${session.conversationId}?visitorToken=${encodeURIComponent(session.visitorToken)}`);
handleConversationUpdate(data, messages, panel, launcher, settings, messageEffectState, { notify: true });
} catch {}
}, 10_000);
}
function handleConversationUpdate(data, messages, panel, launcher, settings, messageEffectState, options = {}) {
if (!data?.messages) return;
const previousAdminMessageId = messageEffectState.knownAdminMessageId || 0;
const latestAdminId = latestAdminMessageId(data.messages);
const hasNewAdminMessage = latestAdminId > previousAdminMessageId;
renderConversation(messages, data);
messageEffectState.knownAdminMessageId = Math.max(previousAdminMessageId, latestAdminId);
if (panel.classList.contains("open")) {
markConversationSeen();
messageEffectState.unreadAdminCount = 0;
updateMessageBadge(launcher, messageEffectState.unreadAdminCount);
clearMessageEffects(launcher);
return;
}
if (options.notify !== false && hasNewAdminMessage) {
messageEffectState.unreadAdminCount += 1;
applyAdminReplyEffects(launcher, settings, messageEffectState.unreadAdminCount);
playNotifySound();
}
}
function latestAdminMessageId(messages) {
return messages.reduce((max, message) => {
if (message.senderType !== "admin") return max;
return Math.max(max, Number(message.id) || 0);
}, 0);
}
function renderConversation(messages, data) {
messages.innerHTML = "";
let lastAdminName = null;
+4 -1
View File
@@ -56,7 +56,10 @@ async function route(req, res) {
if (req.method === "GET" && url.pathname === "/admin") return file(res, path.join(PUBLIC_DIR, "admin.html"), "text/html; charset=utf-8");
if (req.method === "GET" && url.pathname === "/preview") return file(res, path.join(PUBLIC_DIR, "preview.html"), "text/html; charset=utf-8");
if (req.method === "GET" && url.pathname.startsWith("/uploads/")) return serveUpload(res, url.pathname);
if (req.method === "GET" && url.pathname === "/widget.js") return file(res, path.join(PUBLIC_DIR, "widget.js"), "application/javascript; charset=utf-8");
if (req.method === "GET" && url.pathname === "/widget.js") {
res.setHeader("Cache-Control", "no-store, max-age=0");
return file(res, path.join(PUBLIC_DIR, "widget.js"), "application/javascript; charset=utf-8");
}
if (req.method === "GET" && url.pathname.startsWith("/admin.")) return servePublic(res, url.pathname);
if (url.pathname === "/api/admin/login" && req.method === "POST") return adminLogin(req, res);