Strengthen admin reply notifications
This commit is contained in:
+74
-17
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user