Track visitor activity presence
This commit is contained in:
+12
-2
@@ -172,7 +172,7 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; }
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.visitor-name-wrap .online-dot {
|
||||
.visitor-name-wrap .presence-dot {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.unread-dot {
|
||||
@@ -442,13 +442,23 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; }
|
||||
font-weight: 800;
|
||||
font-size: 13px;
|
||||
}
|
||||
.online-dot {
|
||||
.presence-dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.presence-dot.active {
|
||||
background: #2f9e44;
|
||||
box-shadow: 0 0 0 4px rgb(47 158 68 / 14%);
|
||||
}
|
||||
.presence-dot.idle {
|
||||
background: #f59f00;
|
||||
box-shadow: 0 0 0 4px rgb(245 159 0 / 16%);
|
||||
}
|
||||
.presence-dot.offline {
|
||||
background: #c9d4ce;
|
||||
}
|
||||
.visitor-compact-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
+12
-5
@@ -321,7 +321,7 @@ function renderConversations() {
|
||||
<span class="visitor-name-wrap">
|
||||
<span class="unread-dot" aria-label="${item.hasUnread ? "Neprecteno" : "Precteno"}"></span>
|
||||
<strong>${escapeHtml(conversationVisitorName(item))}</strong>
|
||||
${item.visitorOnline ? `<span class="online-dot" aria-label="Online" title="Online"></span>` : ""}
|
||||
${presenceDot(item.visitorPresence)}
|
||||
</span>
|
||||
${item.imageCount ? `<span class="photo-count">foto ${item.imageCount}</span>` : ""}
|
||||
</span>
|
||||
@@ -344,10 +344,11 @@ function renderConversations() {
|
||||
|
||||
function syncConversationVisitors() {
|
||||
if (!state.visitorsLoaded) return;
|
||||
const onlineIds = new Set(state.visitors.map((visitor) => visitor.id));
|
||||
const visitorsById = new Map(state.visitors.map((visitor) => [visitor.id, visitor]));
|
||||
state.conversations = state.conversations.map((item) => ({
|
||||
...item,
|
||||
visitorOnline: onlineIds.has(item.visitorToken)
|
||||
visitorOnline: visitorsById.has(item.visitorToken),
|
||||
visitorPresence: visitorsById.get(item.visitorToken)?.presence || "offline"
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -360,7 +361,7 @@ function renderVisitors() {
|
||||
const botCount = state.visitors.filter((visitor) => visitor.isBot).length;
|
||||
$("#hideBotsToggle").classList.toggle("active", state.hideBots);
|
||||
$("#hideBotsToggle").textContent = state.hideBots ? "Skryt boty" : "Boty videt";
|
||||
$("#visitorCount").textContent = `${visitors.length} online${state.hideBots && botCount ? ` · ${botCount} bot` : ""}`;
|
||||
$("#visitorCount").textContent = `${visitors.length} na webu${state.hideBots && botCount ? ` · ${botCount} bot` : ""}`;
|
||||
$("#visitorList").innerHTML = visitors.length ? visitors.map((visitor) => `
|
||||
<article class="visitor-card ${visitor.lastConversationId ? "clickable" : ""} ${visitor.lastConversationId === state.activeId ? "active" : ""} ${visitor.isBot ? "bot" : ""}" data-visitor-conversation="${escapeHtml(visitor.lastConversationId || "")}">
|
||||
<div class="visitor-card-head">
|
||||
@@ -369,7 +370,7 @@ function renderVisitors() {
|
||||
<strong>${escapeHtml(visitor.label || "Navstevnik")}</strong>
|
||||
<small>${escapeHtml(visitor.isBot ? `bot: ${visitor.botReason || "crawler"}` : browserLabel(visitor.device))}</small>
|
||||
</div>
|
||||
<span class="online-dot" title="Online"></span>
|
||||
${presenceDot(visitor.presence)}
|
||||
</div>
|
||||
<div class="visitor-compact-meta">
|
||||
<span>${visitor.countryFlag || escapeHtml(visitor.countryCode || "?")}</span>
|
||||
@@ -383,6 +384,12 @@ function renderVisitors() {
|
||||
`).join("") : `<p class="empty-list">Zatim nikdo online.</p>`;
|
||||
}
|
||||
|
||||
function presenceDot(presence) {
|
||||
const value = ["active", "idle", "offline"].includes(presence) ? presence : "offline";
|
||||
const label = value === "active" ? "Aktivni" : value === "idle" ? "Neaktivni" : "Offline";
|
||||
return `<span class="presence-dot ${value}" aria-label="${label}" title="${label}"></span>`;
|
||||
}
|
||||
|
||||
function pageTitle(visitor) {
|
||||
const history = Array.isArray(visitor.browsingHistory) ? visitor.browsingHistory : [];
|
||||
const current = history.find((item) => item.url === visitor.currentUrl) || history[history.length - 1];
|
||||
|
||||
+21
-1
@@ -7,6 +7,7 @@
|
||||
const session = loadSession();
|
||||
const allowedImageTypes = new Set(["image/jpeg", "image/png", "image/webp", "image/gif", "image/avif"]);
|
||||
const maxAttachmentBytes = 3_000_000;
|
||||
let lastActivityAt = Date.now();
|
||||
|
||||
fetch(`${apiBase}/api/widget/config?siteKey=${encodeURIComponent(siteKey)}`)
|
||||
.then((res) => res.json())
|
||||
@@ -164,10 +165,14 @@
|
||||
});
|
||||
|
||||
rememberPage();
|
||||
trackVisitorActivity(root);
|
||||
pingVisitor();
|
||||
setInterval(pingVisitor, 25_000);
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (!document.hidden) pingVisitor();
|
||||
if (!document.hidden) {
|
||||
noteActivity();
|
||||
pingVisitor();
|
||||
}
|
||||
});
|
||||
if (session.conversationId) {
|
||||
loadExistingConversation(messages).finally(() => connectEvents(messages, typing, panel, launcher, settings));
|
||||
@@ -340,6 +345,7 @@
|
||||
currentUrl: location.href,
|
||||
referrer: document.referrer,
|
||||
browsingHistory: rememberPage(),
|
||||
lastActivityAt: new Date(lastActivityAt).toISOString(),
|
||||
language: navigator.language,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
device: {
|
||||
@@ -354,6 +360,20 @@
|
||||
postJson(`${apiBase}/api/widget/visitors/ping`, withVisitorContext({})).catch(() => {});
|
||||
}
|
||||
|
||||
function trackVisitorActivity(root) {
|
||||
const passive = { passive: true, capture: true };
|
||||
for (const eventName of ["pointerdown", "keydown", "mousemove", "touchstart", "scroll"]) {
|
||||
document.addEventListener(eventName, noteActivity, passive);
|
||||
root.addEventListener(eventName, noteActivity, passive);
|
||||
}
|
||||
}
|
||||
|
||||
function noteActivity() {
|
||||
const now = Date.now();
|
||||
if (now - lastActivityAt < 1_000) return;
|
||||
lastActivityAt = now;
|
||||
}
|
||||
|
||||
function showFormError(root, message) {
|
||||
const note = root.querySelector(".mf-note");
|
||||
note.textContent = message;
|
||||
|
||||
Reference in New Issue
Block a user