diff --git a/.env.example b/.env.example
index eb97c72..50a2855 100644
--- a/.env.example
+++ b/.env.example
@@ -11,6 +11,7 @@ AUTH_SECRET=change-me-generate-long-random-secret
# Admin users, extend with USER_2/PASS_2, USER_3/PASS_3, ...
USER_1=admin
PASS_1=change-me
+NAME_1=Alex
# Seed first company/site
DEFAULT_COMPANY_NAME=MAAL
diff --git a/public/widget.js b/public/widget.js
index 8b92273..ffd812c 100644
--- a/public/widget.js
+++ b/public/widget.js
@@ -252,13 +252,22 @@
function renderConversation(messages, data) {
messages.innerHTML = "";
+ let lastAdminName = null;
for (const message of data.messages) {
- addMessage(messages, message.senderType, message.body, message.attachments || []);
+ addMessage(messages, message.senderType, message.senderName, message.body, message.attachments || [], lastAdminName);
+ if (message.senderType === "admin") lastAdminName = message.senderName || "Operator";
+ else lastAdminName = null;
}
messages.scrollTop = messages.scrollHeight;
}
- function addMessage(messages, sender, body, attachments) {
+ function addMessage(messages, sender, senderName, body, attachments, lastAdminName) {
+ if (sender === "admin" && (senderName || "Operator") !== lastAdminName) {
+ const label = document.createElement("div");
+ label.className = "mf-operator-label";
+ label.textContent = `${senderName || "Operator"} odpovida`;
+ messages.append(label);
+ }
const item = document.createElement("article");
item.className = `mf-message ${sender}`;
item.innerHTML = `${body ? `
${escapeHtml(body)}
` : ""}${attachments.map((a) => `
`).join("")}`;
@@ -367,6 +376,7 @@
.mf-message { max-width: 86%; padding: 9px 11px; border-radius: 8px; background: #f1f5f2; color: ${c.text}; }
.mf-message.admin { align-self: flex-start; background: #eef7f4; }
.mf-message.visitor { align-self: flex-end; background: ${c.brand}; color: ${c.brandText}; }
+ .mf-operator-label { align-self: flex-start; margin: 4px 0 -4px; color: ${c.muted}; font-size: 12px; font-weight: 700; }
.mf-message p { margin: 0; white-space: pre-wrap; }
.mf-message a { color: inherit; display: inline-block; margin-top: 6px; }
.mf-photo img { display: block; width: min(180px, 100%); max-height: 130px; object-fit: cover; border-radius: 6px; }
diff --git a/server.js b/server.js
index c4c0e41..1a16c44 100644
--- a/server.js
+++ b/server.js
@@ -229,19 +229,24 @@ function configuredUsers() {
for (let i = 1; i < 50; i += 1) {
const username = process.env[`USER_${i}`];
const password = process.env[`PASS_${i}`];
+ const displayName = process.env[`NAME_${i}`] || username;
if (!username && !password) break;
- if (username && password) users.push({ username, password });
+ if (username && password) users.push({ username, password, displayName });
}
return users;
}
+function adminProfile(username) {
+ return configuredUsers().find((user) => user.username === username) || { username, displayName: username };
+}
+
async function adminLogin(req, res) {
const body = await readJson(req);
const match = configuredUsers().find((u) => u.username === body.username && u.password === body.password);
if (!match) return json(res, 401, { error: "bad_credentials" });
const token = signSession(match.username);
res.setHeader("Set-Cookie", `${COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${60 * 60 * 24 * 14}`);
- return json(res, 200, { ok: true, user: { username: match.username } });
+ return json(res, 200, { ok: true, user: { username: match.username, displayName: match.displayName } });
}
function adminLogout(res) {
@@ -253,7 +258,7 @@ function requireAdmin(req, res, next) {
const token = parseCookies(req.headers.cookie || "")[COOKIE_NAME];
const username = token && verifySession(token);
if (!username) return json(res, 401, { error: "unauthorized" });
- req.adminUser = { username };
+ req.adminUser = adminProfile(username);
return next();
}
@@ -423,7 +428,7 @@ async function createAdminMessage(req, res, url) {
if (!attachmentsAreAllowed(body.attachments || [])) return json(res, 400, { error: "only_images_allowed" });
const conversation = findConversation(publicIdFrom(url));
if (!conversation) return json(res, 404, { error: "conversation_not_found" });
- const messageId = insertMessage(conversation.id, "admin", req.adminUser.username, String(body.message || "").trim());
+ const messageId = insertMessage(conversation.id, "admin", req.adminUser.displayName || req.adminUser.username, String(body.message || "").trim());
saveAttachments(conversation.id, messageId, body.attachments || []);
bumpConversation(conversation.id, conversation.status === "new" ? "open" : conversation.status);
markConversationSeen(conversation.id);