Fix widget reply notification effects
This commit is contained in:
+71
-16
@@ -73,7 +73,10 @@
|
|||||||
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 = [];
|
||||||
let unreadAdminCount = 0;
|
const messageEffectState = {
|
||||||
|
unreadAdminCount: 0,
|
||||||
|
knownAdminMessageId: 0
|
||||||
|
};
|
||||||
renderSoundState(soundButton);
|
renderSoundState(soundButton);
|
||||||
|
|
||||||
launcher.addEventListener("click", () => {
|
launcher.addEventListener("click", () => {
|
||||||
@@ -81,8 +84,8 @@
|
|||||||
if (panel.classList.contains("open")) {
|
if (panel.classList.contains("open")) {
|
||||||
launcher.classList.remove("mf-notify");
|
launcher.classList.remove("mf-notify");
|
||||||
clearMessageEffects(launcher);
|
clearMessageEffects(launcher);
|
||||||
unreadAdminCount = 0;
|
messageEffectState.unreadAdminCount = 0;
|
||||||
updateMessageBadge(launcher, unreadAdminCount);
|
updateMessageBadge(launcher, messageEffectState.unreadAdminCount);
|
||||||
scrollMessagesToBottom(messages);
|
scrollMessagesToBottom(messages);
|
||||||
markConversationSeen();
|
markConversationSeen();
|
||||||
}
|
}
|
||||||
@@ -164,8 +167,9 @@
|
|||||||
form.message.value = "";
|
form.message.value = "";
|
||||||
selectedFiles = [];
|
selectedFiles = [];
|
||||||
renderSelectedFiles(attachmentList, selectedFiles);
|
renderSelectedFiles(attachmentList, selectedFiles);
|
||||||
renderConversation(messages, data);
|
handleConversationUpdate(data, messages, panel, launcher, settings, messageEffectState, { notify: false });
|
||||||
connectEvents(messages, typing, panel, launcher, settings);
|
connectEvents(messages, typing, panel, launcher, settings, messageEffectState);
|
||||||
|
startConversationPolling(messages, panel, launcher, settings, messageEffectState);
|
||||||
} catch {
|
} catch {
|
||||||
showFormError(root, "Zpravu se nepodarilo odeslat. Zkuste to prosim znovu.");
|
showFormError(root, "Zpravu se nepodarilo odeslat. Zkuste to prosim znovu.");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -184,7 +188,14 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (session.conversationId) {
|
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;
|
if (session.events || !session.conversationId) return;
|
||||||
session.events = new EventSource(`${apiBase}/api/widget/conversations/${session.conversationId}/events`);
|
session.events = new EventSource(`${apiBase}/api/widget/conversations/${session.conversationId}/events`);
|
||||||
session.events.addEventListener("message", (event) => {
|
session.events.addEventListener("message", (event) => {
|
||||||
@@ -207,17 +218,17 @@
|
|||||||
connectEvents.typingTimer = setTimeout(() => { typing.hidden = true; }, 1800);
|
connectEvents.typingTimer = setTimeout(() => { typing.hidden = true; }, 1800);
|
||||||
}
|
}
|
||||||
if (data.type === "message:new") {
|
if (data.type === "message:new") {
|
||||||
renderConversation(messages, data.payload);
|
handleConversationUpdate(data.payload, messages, panel, launcher, settings, messageEffectState, { notify: true });
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
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) {
|
function createHeaderActions(root, closeButton) {
|
||||||
@@ -283,12 +294,56 @@
|
|||||||
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)}`);
|
||||||
renderConversation(messages, data);
|
renderConversation(messages, data);
|
||||||
if (panel.classList.contains("open")) markConversationSeen();
|
if (panel.classList.contains("open")) markConversationSeen();
|
||||||
|
return data;
|
||||||
} catch {
|
} catch {
|
||||||
session.conversationId = null;
|
session.conversationId = null;
|
||||||
saveSession();
|
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) {
|
function renderConversation(messages, data) {
|
||||||
messages.innerHTML = "";
|
messages.innerHTML = "";
|
||||||
let lastAdminName = null;
|
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 === "/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 === "/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.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 (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);
|
if (url.pathname === "/api/admin/login" && req.method === "POST") return adminLogin(req, res);
|
||||||
|
|||||||
Reference in New Issue
Block a user