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,
events: null,
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);
@@ -41,6 +45,11 @@ $("#logoutButton").addEventListener("click", async () => {
$("#settingsButton").addEventListener("click", () => $("#settingsPanel").classList.remove("hidden"));
$("#closeSettingsButton").addEventListener("click", () => $("#settingsPanel").classList.add("hidden"));
window.addEventListener("focus", stopAttention);
document.addEventListener("visibilitychange", () => {
if (!document.hidden) stopAttention();
});
$("#onlineToggle").addEventListener("change", async () => {
await saveSite({ isOnline: $("#onlineToggle").checked });
});
@@ -69,26 +78,31 @@ $("#settingsForm").addEventListener("submit", async (event) => {
$("#replyForm").addEventListener("submit", async (event) => {
event.preventDefault();
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 form = new FormData(event.currentTarget);
const form = new FormData(replyForm);
const payload = {
message: form.get("message") || "",
attachments: await Promise.all(state.replyFiles.slice(0, 3).map(fileToPayload))
};
if (!payload.message.trim() && !payload.attachments.length) return;
textarea.value = "";
clearReplyComposer(replyForm);
try {
await api(`/api/admin/conversations/${state.activeId}/messages`, {
method: "POST",
body: payload
});
event.currentTarget.reset();
clearReplyComposer(replyForm);
setTimeout(() => clearReplyComposer(replyForm), 0);
setTimeout(() => clearReplyComposer(replyForm), 120);
state.replyFiles = [];
renderReplyAttachments();
await openConversation(state.activeId);
clearReplyComposer(replyForm);
} catch (error) {
textarea.value = originalMessage;
textarea.defaultValue = originalMessage;
throw error;
}
});
@@ -207,6 +221,7 @@ async function loadConversations() {
}
async function openConversation(id) {
stopAttention();
state.activeId = id;
const data = await api(`/api/admin/conversations/${id}`);
state.active = data;
@@ -234,7 +249,7 @@ function connectEvents() {
return;
}
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("");
}
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) {
const allowedTypes = new Set(["image/jpeg", "image/png", "image/webp", "image/gif", "image/avif"]);
const nextFiles = [...currentFiles];
@@ -544,19 +568,52 @@ async function saveSite(patch) {
}
function notify() {
document.title = "* Nova zprava - MaalFlows";
const audio = new AudioContext();
const osc = audio.createOscillator();
startAttention();
playNotifySound();
}
function playNotifySound() {
const AudioCtor = window.AudioContext || window.webkitAudioContext;
if (!AudioCtor) return;
const audio = new AudioCtor();
const gain = audio.createGain();
osc.frequency.value = 880;
gain.gain.value = 0.04;
osc.connect(gain);
gain.gain.value = 0.055;
gain.connect(audio.destination);
osc.start();
setTimeout(() => {
osc.stop();
audio.close();
}, 160);
[0, 0.14].forEach((offset, index) => {
const osc = audio.createOscillator();
osc.type = "sine";
osc.frequency.value = index ? 1040 : 820;
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() {