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