Strengthen admin reply notifications

This commit is contained in:
rajchales-monito
2026-07-23 23:42:36 +02:00
parent fdfbf5ecc4
commit 5b2314a11e
+74 -17
View File
@@ -6,7 +6,11 @@ const state = {
active: null, active: null,
events: null, events: null,
statusFilter: "active", statusFilter: "active",
replyFiles: [] replyFiles: [],
attentionTimer: null,
attentionOn: false,
originalTitle: document.title,
originalFavicon: document.querySelector("link[rel='icon']")?.href || "/favicon.svg"
}; };
const $ = (selector) => document.querySelector(selector); const $ = (selector) => document.querySelector(selector);
@@ -41,6 +45,11 @@ $("#logoutButton").addEventListener("click", async () => {
$("#settingsButton").addEventListener("click", () => $("#settingsPanel").classList.remove("hidden")); $("#settingsButton").addEventListener("click", () => $("#settingsPanel").classList.remove("hidden"));
$("#closeSettingsButton").addEventListener("click", () => $("#settingsPanel").classList.add("hidden")); $("#closeSettingsButton").addEventListener("click", () => $("#settingsPanel").classList.add("hidden"));
window.addEventListener("focus", stopAttention);
document.addEventListener("visibilitychange", () => {
if (!document.hidden) stopAttention();
});
$("#onlineToggle").addEventListener("change", async () => { $("#onlineToggle").addEventListener("change", async () => {
await saveSite({ isOnline: $("#onlineToggle").checked }); await saveSite({ isOnline: $("#onlineToggle").checked });
}); });
@@ -69,26 +78,31 @@ $("#settingsForm").addEventListener("submit", async (event) => {
$("#replyForm").addEventListener("submit", async (event) => { $("#replyForm").addEventListener("submit", async (event) => {
event.preventDefault(); event.preventDefault();
if (!state.activeId) return; if (!state.activeId) return;
const textarea = event.currentTarget.querySelector("textarea[name='message']"); const replyForm = event.currentTarget;
const textarea = replyForm.querySelector("textarea[name='message']");
const originalMessage = textarea.value; const originalMessage = textarea.value;
const form = new FormData(event.currentTarget); const form = new FormData(replyForm);
const payload = { const payload = {
message: form.get("message") || "", message: form.get("message") || "",
attachments: await Promise.all(state.replyFiles.slice(0, 3).map(fileToPayload)) attachments: await Promise.all(state.replyFiles.slice(0, 3).map(fileToPayload))
}; };
if (!payload.message.trim() && !payload.attachments.length) return; if (!payload.message.trim() && !payload.attachments.length) return;
textarea.value = ""; clearReplyComposer(replyForm);
try { try {
await api(`/api/admin/conversations/${state.activeId}/messages`, { await api(`/api/admin/conversations/${state.activeId}/messages`, {
method: "POST", method: "POST",
body: payload body: payload
}); });
event.currentTarget.reset(); clearReplyComposer(replyForm);
setTimeout(() => clearReplyComposer(replyForm), 0);
setTimeout(() => clearReplyComposer(replyForm), 120);
state.replyFiles = []; state.replyFiles = [];
renderReplyAttachments(); renderReplyAttachments();
await openConversation(state.activeId); await openConversation(state.activeId);
clearReplyComposer(replyForm);
} catch (error) { } catch (error) {
textarea.value = originalMessage; textarea.value = originalMessage;
textarea.defaultValue = originalMessage;
throw error; throw error;
} }
}); });
@@ -207,6 +221,7 @@ async function loadConversations() {
} }
async function openConversation(id) { async function openConversation(id) {
stopAttention();
state.activeId = id; state.activeId = id;
const data = await api(`/api/admin/conversations/${id}`); const data = await api(`/api/admin/conversations/${id}`);
state.active = data; state.active = data;
@@ -234,7 +249,7 @@ function connectEvents() {
return; return;
} }
if (state.activeId) await openConversation(state.activeId).catch(() => clearActiveConversation()); if (state.activeId) await openConversation(state.activeId).catch(() => clearActiveConversation());
if (data.type === "conversation:new" || data.type === "message:new") notify(); if (data.type === "conversation:new" || isVisitorMessageEvent(data)) notify();
}); });
} }
@@ -443,6 +458,15 @@ function renderReplyAttachments() {
`).join(""); `).join("");
} }
function clearReplyComposer(form = $("#replyForm")) {
const textarea = form?.querySelector("textarea[name='message']");
if (!textarea) return;
textarea.value = "";
textarea.defaultValue = "";
textarea.textContent = "";
textarea.dispatchEvent(new Event("input", { bubbles: true }));
}
function mergeImageFiles(currentFiles, incomingFiles) { function mergeImageFiles(currentFiles, incomingFiles) {
const allowedTypes = new Set(["image/jpeg", "image/png", "image/webp", "image/gif", "image/avif"]); const allowedTypes = new Set(["image/jpeg", "image/png", "image/webp", "image/gif", "image/avif"]);
const nextFiles = [...currentFiles]; const nextFiles = [...currentFiles];
@@ -544,19 +568,52 @@ async function saveSite(patch) {
} }
function notify() { function notify() {
document.title = "* Nova zprava - MaalFlows"; startAttention();
const audio = new AudioContext(); playNotifySound();
const osc = audio.createOscillator(); }
function playNotifySound() {
const AudioCtor = window.AudioContext || window.webkitAudioContext;
if (!AudioCtor) return;
const audio = new AudioCtor();
const gain = audio.createGain(); const gain = audio.createGain();
osc.frequency.value = 880; gain.gain.value = 0.055;
gain.gain.value = 0.04;
osc.connect(gain);
gain.connect(audio.destination); gain.connect(audio.destination);
osc.start(); [0, 0.14].forEach((offset, index) => {
setTimeout(() => { const osc = audio.createOscillator();
osc.stop(); osc.type = "sine";
audio.close(); osc.frequency.value = index ? 1040 : 820;
}, 160); osc.connect(gain);
osc.start(audio.currentTime + offset);
osc.stop(audio.currentTime + offset + 0.11);
});
setTimeout(() => audio.close().catch(() => {}), 520);
}
function startAttention() {
if (state.attentionTimer) return;
const favicon = document.querySelector("link[rel='icon']");
const alertIcon = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#0f8f6f"/><circle cx="46" cy="18" r="11" fill="#ff3b30"/><path fill="#fff" d="M18 24h22v6H18zm0 11h16v6H18z"/></svg>')}`;
state.attentionTimer = setInterval(() => {
state.attentionOn = !state.attentionOn;
document.title = state.attentionOn ? "* Nova zprava - MaalFlows" : state.originalTitle;
if (favicon) favicon.href = state.attentionOn ? alertIcon : state.originalFavicon;
}, 760);
}
function stopAttention() {
if (state.attentionTimer) clearInterval(state.attentionTimer);
state.attentionTimer = null;
state.attentionOn = false;
document.title = state.originalTitle;
const favicon = document.querySelector("link[rel='icon']");
if (favicon) favicon.href = state.originalFavicon;
}
function isVisitorMessageEvent(data) {
if (data.type !== "message:new") return false;
const messages = data.payload?.messages || [];
return messages[messages.length - 1]?.senderType === "visitor";
} }
function flashTyping() { function flashTyping() {