Add visitor read receipts

This commit is contained in:
rajchales-monito
2026-07-24 12:26:42 +02:00
parent f052bca3ad
commit 627891d0d0
4 changed files with 53 additions and 3 deletions
+15
View File
@@ -68,6 +68,7 @@ async function route(req, res) {
if (url.pathname === "/api/widget/conversations" && req.method === "POST") return createConversation(req, res);
if (url.pathname.match(/^\/api\/widget\/conversations\/[^/]+$/) && req.method === "GET") return getVisitorConversation(res, url);
if (url.pathname.match(/^\/api\/widget\/conversations\/[^/]+\/messages$/) && req.method === "POST") return createVisitorMessage(req, res, url);
if (url.pathname.match(/^\/api\/widget\/conversations\/[^/]+\/seen$/) && req.method === "POST") return markVisitorSeen(req, res, url);
if (url.pathname.match(/^\/api\/widget\/conversations\/[^/]+\/events$/) && req.method === "GET") return eventStream(req, res, url, "visitor");
if (url.pathname.match(/^\/api\/widget\/conversations\/[^/]+\/typing$/) && req.method === "POST") return visitorTyping(req, res, url);
@@ -137,6 +138,7 @@ function initDb() {
ip TEXT,
country_code TEXT,
vpn_risk INTEGER NOT NULL DEFAULT 0,
visitor_seen_at TEXT,
last_message_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
@@ -192,6 +194,7 @@ function initDb() {
if (ensureColumn("visitor_sessions", "last_activity_at", "TEXT")) {
db.prepare("UPDATE visitor_sessions SET last_activity_at = last_seen_at WHERE last_activity_at IS NULL").run();
}
ensureColumn("conversations", "visitor_seen_at", "TEXT");
}
function ensureColumn(table, column, definition) {
@@ -515,6 +518,17 @@ async function createVisitorMessage(req, res, url) {
return json(res, 201, conversationDetails(updated));
}
async function markVisitorSeen(req, res, url) {
const body = await readJson(req).catch(() => ({}));
const conversation = findConversation(publicIdFrom(url));
if (!conversation) return json(res, 404, { error: "conversation_not_found" });
if (conversation.visitor_token !== body.visitorToken) return json(res, 403, { error: "forbidden" });
db.prepare("UPDATE conversations SET visitor_seen_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(conversation.id);
const updated = findConversation(conversation.public_id);
publishConversation(updated, { type: "conversation:seen", conversation: formatConversationSummary(updated) });
return json(res, 200, { ok: true, conversation: formatConversationSummary(updated) });
}
async function createAdminMessage(req, res, url) {
const body = await readJson(req, 8_000_000);
if ((!body.message || !String(body.message).trim()) && !(body.attachments || []).length) return json(res, 400, { error: "message_or_image_required" });
@@ -734,6 +748,7 @@ function formatConversationSummary(row) {
visitorLabel: visitorLabel(row.visitor_token),
visitorOnline: Boolean(row.visitor_online),
visitorPresence: row.visitor_presence || (row.visitor_online ? "active" : "offline"),
visitorSeenAt: row.visitor_seen_at,
visitor: {
name: row.visitor_name,
email: row.visitor_email,