Show operator presence in admin chats
This commit is contained in:
@@ -43,6 +43,7 @@ initDb();
|
||||
seedDefaults();
|
||||
|
||||
const streams = new Map();
|
||||
const operatorPresence = new Map();
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
@@ -112,6 +113,7 @@ async function adminApi(req, res, url) {
|
||||
if (url.pathname.match(/^\/api\/admin\/conversations\/[^/]+\/messages$/) && req.method === "POST") return createAdminMessage(req, res, url);
|
||||
if (url.pathname.match(/^\/api\/admin\/conversations\/[^/]+\/status$/) && req.method === "PATCH") return updateConversationStatus(req, res, url);
|
||||
if (url.pathname.match(/^\/api\/admin\/conversations\/[^/]+\/typing$/) && req.method === "POST") return adminTyping(req, res, url);
|
||||
if (url.pathname.match(/^\/api\/admin\/conversations\/[^/]+\/presence$/) && req.method === "POST") return adminPresence(req, res, url);
|
||||
return json(res, 404, { error: "not_found" });
|
||||
}
|
||||
|
||||
@@ -967,10 +969,65 @@ async function visitorTyping(req, res, url) {
|
||||
async function adminTyping(req, res, url) {
|
||||
const conversation = findConversation(publicIdFrom(url));
|
||||
if (!conversation) return json(res, 404, { error: "conversation_not_found" });
|
||||
publishConversation(conversation, { type: "typing", actor: "admin", conversationId: conversation.public_id });
|
||||
touchOperatorPresence(conversation.public_id, req.adminUser);
|
||||
publishConversation(conversation, {
|
||||
type: "typing",
|
||||
actor: "admin",
|
||||
conversationId: conversation.public_id,
|
||||
username: req.adminUser.username,
|
||||
name: operatorDisplayName(req.adminUser)
|
||||
});
|
||||
publish("admin", {
|
||||
type: "operator:presence",
|
||||
conversationId: conversation.public_id,
|
||||
operators: currentOperatorPresence(conversation.public_id)
|
||||
});
|
||||
return json(res, 200, { ok: true });
|
||||
}
|
||||
|
||||
async function adminPresence(req, res, url) {
|
||||
const conversation = findConversation(publicIdFrom(url));
|
||||
if (!conversation) return json(res, 404, { error: "conversation_not_found" });
|
||||
touchOperatorPresence(conversation.public_id, req.adminUser);
|
||||
const operators = currentOperatorPresence(conversation.public_id);
|
||||
publish("admin", { type: "operator:presence", conversationId: conversation.public_id, operators });
|
||||
return json(res, 200, { ok: true, operators });
|
||||
}
|
||||
|
||||
function touchOperatorPresence(conversationId, adminUser) {
|
||||
cleanupOperatorPresence();
|
||||
const username = adminUser.username || "operator";
|
||||
operatorPresence.set(`${conversationId}:${username}`, {
|
||||
conversationId,
|
||||
username,
|
||||
name: operatorDisplayName(adminUser),
|
||||
lastSeenAt: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
function operatorDisplayName(adminUser = {}) {
|
||||
return adminUser.chatNick || adminUser.displayName || adminUser.username || "Operator";
|
||||
}
|
||||
|
||||
function currentOperatorPresence(conversationId) {
|
||||
cleanupOperatorPresence();
|
||||
return [...operatorPresence.values()]
|
||||
.filter((item) => item.conversationId === conversationId)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((item) => ({
|
||||
username: item.username,
|
||||
name: item.name,
|
||||
lastSeenAt: new Date(item.lastSeenAt).toISOString()
|
||||
}));
|
||||
}
|
||||
|
||||
function cleanupOperatorPresence() {
|
||||
const now = Date.now();
|
||||
for (const [key, item] of operatorPresence.entries()) {
|
||||
if (now - item.lastSeenAt > 45_000) operatorPresence.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
function eventStream(req, res, url, channel) {
|
||||
const key = channel === "admin" ? "admin" : `conversation:${publicIdFrom(url)}`;
|
||||
res.writeHead(200, {
|
||||
@@ -1731,6 +1788,7 @@ function conversationDetails(conversation) {
|
||||
const attachments = db.prepare("SELECT * FROM attachments WHERE conversation_id = ? ORDER BY id ASC").all(conversation.id);
|
||||
return {
|
||||
conversation: formatConversationSummary(conversation),
|
||||
operators: currentOperatorPresence(conversation.public_id),
|
||||
messages: messages.map((message) => ({
|
||||
id: message.id,
|
||||
senderType: message.sender_type,
|
||||
|
||||
Reference in New Issue
Block a user