Fix widget reply notification effects
This commit is contained in:
+71
-16
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user