Initial import of f7support application.
This commit is contained in:
+616
@@ -0,0 +1,616 @@
|
||||
/**
|
||||
* Производительность: список тикетов — после первого кадра (requestIdleCallback / queueMicrotask);
|
||||
* кэш списка в sessionStorage на время вкладки; обновление при возврате фокуса.
|
||||
* Скрипт подключается через Util::addScript — ядро NC уже даёт defer и ?v=… (версия приложения / хэш).
|
||||
* «Живой» чат без опроса — только WebSocket/SSE на apiBase (отдельная задача бэкенда).
|
||||
*/
|
||||
(function () {
|
||||
const root = document.getElementById("f7support-app");
|
||||
if (!root) return;
|
||||
|
||||
const username = root.dataset.username;
|
||||
const serverAddress = root.dataset.serverAddress;
|
||||
const apiBase = root.dataset.supportApiBase;
|
||||
const messagesPollMs = Math.max(2000, parseInt(root.dataset.messagesPollMs, 10) || 5000);
|
||||
|
||||
/** @type {ReturnType<typeof setInterval> | null} */
|
||||
let chatMessagesPollTimer = null;
|
||||
|
||||
const state = {
|
||||
tickets: [],
|
||||
currentTicket: null,
|
||||
messages: [],
|
||||
/** Preview images in the message list (revoked on re-render / close chat). */
|
||||
/** @type {string[]} */
|
||||
previewBlobUrls: [],
|
||||
};
|
||||
|
||||
function clientHeaders(extra) {
|
||||
const h = {
|
||||
"X-F7cloud-User": username,
|
||||
"X-F7cloud-Server": serverAddress,
|
||||
};
|
||||
if (extra && typeof extra === "object") {
|
||||
Object.assign(h, extra);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
function revokePreviewBlobs() {
|
||||
for (const u of state.previewBlobUrls) {
|
||||
try {
|
||||
URL.revokeObjectURL(u);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
state.previewBlobUrls = [];
|
||||
}
|
||||
|
||||
async function formatApiError(response, fallback) {
|
||||
try {
|
||||
const data = await response.clone().json();
|
||||
const d = data?.detail;
|
||||
if (Array.isArray(d)) {
|
||||
return d.map((e) => e.msg || JSON.stringify(e)).join("; ") || fallback;
|
||||
}
|
||||
if (typeof d === "string") return d;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function formatBytes(n) {
|
||||
if (n == null || Number.isNaN(n)) return "";
|
||||
const u = ["байт", "КБ", "МБ", "ГБ"];
|
||||
let v = n;
|
||||
let i = 0;
|
||||
while (v >= 1024 && i < u.length - 1) {
|
||||
v /= 1024;
|
||||
i++;
|
||||
}
|
||||
return `${v < 10 && i > 0 ? v.toFixed(1) : Math.round(v)} ${u[i]}`;
|
||||
}
|
||||
|
||||
root.innerHTML = `
|
||||
<div class="f7s-main">
|
||||
<h2>f7support</h2>
|
||||
<button id="create-ticket-btn" type="button">Создать токен</button>
|
||||
<p id="error-box" class="f7s-error"></p>
|
||||
<h3 class="f7s-section-title">Мои токены</h3>
|
||||
<p id="ticket-hint" class="f7s-hint">Нажмите на токен, чтобы открыть чат. Закройте окно чата, чтобы выбрать другой токен.</p>
|
||||
<ul id="ticket-list" class="f7s-ticket-list"></ul>
|
||||
</div>
|
||||
|
||||
<div id="create-ticket-modal" class="f7s-modal f7s-modal--create">
|
||||
<div class="f7s-modal-panel">
|
||||
<div class="f7s-modal-head">
|
||||
<h3>Создание токена</h3>
|
||||
<button type="button" id="close-modal-btn" class="f7s-btn-icon" aria-label="Закрыть">✕</button>
|
||||
</div>
|
||||
<input id="subject-input" class="f7s-input" maxlength="255" placeholder="Тема" />
|
||||
<textarea id="body-input" class="f7s-input f7s-textarea" maxlength="4000" placeholder="Опишите проблему (до 4000 символов)"></textarea>
|
||||
<div class="f7s-chat-actions f7s-mt-8">
|
||||
<input id="create-ticket-attachment-input" type="file" multiple />
|
||||
</div>
|
||||
<button type="button" id="submit-ticket-btn" class="f7s-mt-8">Отправить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="chat-modal" class="f7s-modal f7s-modal--chat">
|
||||
<div class="f7s-chat-shell" role="dialog" aria-modal="true" aria-labelledby="chat-title">
|
||||
<div class="f7s-chat-head">
|
||||
<h3 id="chat-title" class="f7s-chat-title">Чат</h3>
|
||||
<button type="button" id="close-chat-btn" class="f7s-btn-icon f7s-btn-icon--lg" aria-label="Закрыть чат">✕</button>
|
||||
</div>
|
||||
<p id="chat-error-box" class="f7s-error f7s-chat-error"></p>
|
||||
<div id="message-list" class="f7s-message-list"></div>
|
||||
<div class="f7s-chat-footer">
|
||||
<textarea id="message-input" class="f7s-input f7s-textarea f7s-textarea--message" maxlength="4000" placeholder="Сообщение (до 4000 символов)"></textarea>
|
||||
<div class="f7s-chat-actions">
|
||||
<input id="attachment-input" type="file" />
|
||||
<button type="button" id="send-message-btn">Отправить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const ticketList = document.getElementById("ticket-list");
|
||||
const messageList = document.getElementById("message-list");
|
||||
const chatTitle = document.getElementById("chat-title");
|
||||
const errorBox = document.getElementById("error-box");
|
||||
const chatErrorBox = document.getElementById("chat-error-box");
|
||||
const createModal = document.getElementById("create-ticket-modal");
|
||||
const chatModal = document.getElementById("chat-modal");
|
||||
|
||||
function showError(message) {
|
||||
errorBox.textContent = message || "";
|
||||
}
|
||||
|
||||
function showChatError(message) {
|
||||
chatErrorBox.textContent = message || "";
|
||||
}
|
||||
|
||||
function isChatOpen() {
|
||||
return chatModal.classList.contains("f7s-open");
|
||||
}
|
||||
|
||||
function openChatModal() {
|
||||
chatModal.classList.add("f7s-open");
|
||||
}
|
||||
|
||||
function stopChatMessagesPoll() {
|
||||
if (chatMessagesPollTimer !== null) {
|
||||
clearInterval(chatMessagesPollTimer);
|
||||
chatMessagesPollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startChatMessagesPoll() {
|
||||
stopChatMessagesPoll();
|
||||
chatMessagesPollTimer = setInterval(() => {
|
||||
if (document.hidden) return;
|
||||
if (!state.currentTicket || !isChatOpen()) return;
|
||||
void loadMessages(false, false);
|
||||
}, messagesPollMs);
|
||||
}
|
||||
|
||||
function messagesFingerprint(list) {
|
||||
if (!Array.isArray(list)) return "";
|
||||
return list
|
||||
.map((m) => {
|
||||
const attIds = (Array.isArray(m.attachments) ? m.attachments : [])
|
||||
.map((a) => a.id)
|
||||
.join(",");
|
||||
return `${m.id}|${m.created_at ?? ""}|${(m.text || "").length}|${attIds}`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} force — всегда перерисовать (после отправки / первое открытие)
|
||||
* @param {boolean} showErrors — показывать ошибку в UI (только при явной загрузке)
|
||||
*/
|
||||
async function loadMessages(force, showErrors) {
|
||||
if (!state.currentTicket) return;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${apiBase}/api/client/tickets/${encodeURIComponent(state.currentTicket)}/messages`,
|
||||
{ headers: clientHeaders() }
|
||||
);
|
||||
if (!response.ok) {
|
||||
if (showErrors) throw new Error("Не удалось загрузить сообщения");
|
||||
return;
|
||||
}
|
||||
const next = await response.json();
|
||||
if (!force && messagesFingerprint(next) === messagesFingerprint(state.messages)) {
|
||||
return;
|
||||
}
|
||||
state.messages = next;
|
||||
await renderMessages();
|
||||
} catch (e) {
|
||||
if (showErrors) throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function closeChatModal() {
|
||||
stopChatMessagesPoll();
|
||||
revokePreviewBlobs();
|
||||
chatModal.classList.remove("f7s-open");
|
||||
state.currentTicket = null;
|
||||
state.messages = [];
|
||||
messageList.innerHTML = "";
|
||||
const msgInput = document.getElementById("message-input");
|
||||
const fileInput = document.getElementById("attachment-input");
|
||||
if (msgInput) msgInput.value = "";
|
||||
if (fileInput) fileInput.value = "";
|
||||
showChatError("");
|
||||
chatTitle.textContent = "Чат";
|
||||
}
|
||||
|
||||
function ticketsStorageKey() {
|
||||
return `f7support.tickets.v1|${username}|${serverAddress}`;
|
||||
}
|
||||
|
||||
function readTicketsCache() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(ticketsStorageKey());
|
||||
if (!raw) return null;
|
||||
const j = JSON.parse(raw);
|
||||
return Array.isArray(j) ? j : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeTicketsCache(list) {
|
||||
try {
|
||||
sessionStorage.setItem(ticketsStorageKey(), JSON.stringify(list));
|
||||
} catch {
|
||||
/* quota / private mode */
|
||||
}
|
||||
}
|
||||
|
||||
function applyTickets(list) {
|
||||
state.tickets = list;
|
||||
renderTickets();
|
||||
}
|
||||
|
||||
async function fetchTicketsFromNetwork() {
|
||||
const response = await fetch(`${apiBase}/api/client/tickets`, {
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
if (!response.ok) throw new Error("Не удалось получить токены");
|
||||
const list = await response.json();
|
||||
writeTicketsCache(list);
|
||||
applyTickets(list);
|
||||
}
|
||||
|
||||
function scheduleTicketsBootstrap() {
|
||||
const cached = readTicketsCache();
|
||||
if (cached) {
|
||||
applyTickets(cached);
|
||||
}
|
||||
const run = () => {
|
||||
fetchTicketsFromNetwork().catch((e) => {
|
||||
if (!cached) {
|
||||
showError(e.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
if (typeof requestIdleCallback !== "undefined") {
|
||||
requestIdleCallback(run, { timeout: 2500 });
|
||||
} else {
|
||||
queueMicrotask(() => setTimeout(run, 0));
|
||||
}
|
||||
}
|
||||
|
||||
function renderTickets() {
|
||||
ticketList.innerHTML = "";
|
||||
for (const ticket of state.tickets) {
|
||||
const li = document.createElement("li");
|
||||
li.className = "f7s-ticket-item";
|
||||
li.textContent = `${ticket.ticket_number} — ${ticket.subject} [${ticket.status}]`;
|
||||
li.onclick = () => {
|
||||
if (isChatOpen()) {
|
||||
return;
|
||||
}
|
||||
void openTicketChat(ticket.ticket_number);
|
||||
};
|
||||
ticketList.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
async function openTicketChat(ticketNumber) {
|
||||
showError("");
|
||||
state.currentTicket = ticketNumber;
|
||||
chatTitle.textContent = `Чат #${ticketNumber}`;
|
||||
showChatError("");
|
||||
openChatModal();
|
||||
try {
|
||||
await loadMessages(true, true);
|
||||
startChatMessagesPoll();
|
||||
} catch (e) {
|
||||
showChatError(e.message || "Ошибка загрузки");
|
||||
startChatMessagesPoll();
|
||||
}
|
||||
}
|
||||
|
||||
function attachmentDownloadUrl(ticketNumber, attachmentId) {
|
||||
return `${apiBase}/api/client/tickets/${encodeURIComponent(ticketNumber)}/attachments/${encodeURIComponent(String(attachmentId))}`;
|
||||
}
|
||||
|
||||
async function fetchAttachmentBlob(ticketNumber, attachmentId) {
|
||||
const res = await fetch(attachmentDownloadUrl(ticketNumber, attachmentId), {
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
if (!res.ok) throw new Error("Не удалось загрузить вложение");
|
||||
return res.blob();
|
||||
}
|
||||
|
||||
async function renderMessages() {
|
||||
revokePreviewBlobs();
|
||||
messageList.innerHTML = "";
|
||||
const ticketNo = state.currentTicket;
|
||||
if (!ticketNo) return;
|
||||
|
||||
for (const m of state.messages) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "f7s-msg-row";
|
||||
|
||||
const line = document.createElement("div");
|
||||
const b = document.createElement("b");
|
||||
b.textContent = m.author ?? "";
|
||||
line.appendChild(b);
|
||||
line.appendChild(document.createTextNode(` (${m.author_role ?? ""}): `));
|
||||
const textPart = document.createElement("span");
|
||||
textPart.textContent = m.text ?? "";
|
||||
line.appendChild(textPart);
|
||||
row.appendChild(line);
|
||||
|
||||
const attachments = Array.isArray(m.attachments) ? m.attachments : [];
|
||||
for (const att of attachments) {
|
||||
const mime = typeof att.mime_type === "string" ? att.mime_type : "";
|
||||
const filename = att.filename || "file";
|
||||
const sizePart = att.size_bytes != null ? ` · ${formatBytes(att.size_bytes)}` : "";
|
||||
|
||||
if (mime.startsWith("image/")) {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "f7s-img-wrap";
|
||||
const img = document.createElement("img");
|
||||
img.className = "f7s-msg-img";
|
||||
img.alt = filename;
|
||||
wrap.appendChild(img);
|
||||
row.appendChild(wrap);
|
||||
try {
|
||||
const blob = await fetchAttachmentBlob(ticketNo, att.id);
|
||||
const u = URL.createObjectURL(blob);
|
||||
state.previewBlobUrls.push(u);
|
||||
img.src = u;
|
||||
} catch {
|
||||
const err = document.createElement("div");
|
||||
err.className = "f7s-img-err";
|
||||
err.textContent = `Не удалось показать изображение: ${filename}`;
|
||||
wrap.appendChild(err);
|
||||
}
|
||||
} else {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "f7s-attach-row";
|
||||
const label = document.createElement("span");
|
||||
label.textContent = `${filename}${sizePart}`;
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.textContent = "Скачать";
|
||||
btn.onclick = async () => {
|
||||
let objectUrl = null;
|
||||
try {
|
||||
const blob = await fetchAttachmentBlob(ticketNo, att.id);
|
||||
objectUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = objectUrl;
|
||||
a.download = filename;
|
||||
a.rel = "noopener";
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(() => {
|
||||
try {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, 60_000);
|
||||
} catch {
|
||||
showChatError("Не удалось скачать вложение");
|
||||
if (objectUrl) {
|
||||
try {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
wrap.appendChild(label);
|
||||
wrap.appendChild(btn);
|
||||
row.appendChild(wrap);
|
||||
}
|
||||
}
|
||||
|
||||
messageList.appendChild(row);
|
||||
}
|
||||
messageList.scrollTop = messageList.scrollHeight;
|
||||
}
|
||||
|
||||
/** OpenAPI TicketCreate требует body minLength:1; реальный текст уходит первым сообщением в чат. */
|
||||
const TICKET_CREATE_BODY_PLACEHOLDER = ".";
|
||||
|
||||
/**
|
||||
* POST текстового сообщения в тикет (как в форме чата).
|
||||
* @returns {Promise<object>} тело ответа API
|
||||
*/
|
||||
async function postMessageToTicket(ticketNumber, text) {
|
||||
const response = await fetch(
|
||||
`${apiBase}/api/client/tickets/${encodeURIComponent(ticketNumber)}/messages`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: clientHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify({ text }),
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
const msg = await formatApiError(response, "Не удалось отправить сообщение");
|
||||
throw new Error(msg);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* POST одного файла к сообщению (message_id в query).
|
||||
* @param {string|number} messageId
|
||||
* @param {File} file
|
||||
*/
|
||||
async function uploadAttachmentForMessage(ticketNumber, messageId, file) {
|
||||
const form = new FormData();
|
||||
form.append("file", file);
|
||||
const q = new URLSearchParams({ message_id: String(messageId) });
|
||||
const attRes = await fetch(
|
||||
`${apiBase}/api/client/tickets/${encodeURIComponent(ticketNumber)}/attachments?${q.toString()}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: clientHeaders(),
|
||||
body: form,
|
||||
}
|
||||
);
|
||||
if (!attRes.ok) {
|
||||
const msg = await formatApiError(attRes, "Вложение не принято");
|
||||
throw new Error(msg);
|
||||
}
|
||||
return attRes;
|
||||
}
|
||||
|
||||
async function submitTicketWithRetry(payload) {
|
||||
const maxAttempts = 3;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
const duplicate = attempt > 1 ? 1 : 0;
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 180000);
|
||||
try {
|
||||
const response = await fetch(`${apiBase}/api/client/tickets`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ...payload, duplicate }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
if (!response.ok) throw new Error("Send failed");
|
||||
return await response.json();
|
||||
} catch (e) {
|
||||
clearTimeout(timeout);
|
||||
if (attempt === maxAttempts) {
|
||||
throw new Error("Сервер поддержки недоступен, пожалуйста обратитесь к администратору.");
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error("Unknown error");
|
||||
}
|
||||
|
||||
document.getElementById("close-chat-btn").onclick = () => {
|
||||
closeChatModal();
|
||||
};
|
||||
|
||||
document.getElementById("create-ticket-btn").onclick = () => {
|
||||
createModal.classList.add("f7s-open");
|
||||
showError("");
|
||||
};
|
||||
|
||||
document.getElementById("close-modal-btn").onclick = () => {
|
||||
document.getElementById("subject-input").value = "";
|
||||
document.getElementById("body-input").value = "";
|
||||
const catt = document.getElementById("create-ticket-attachment-input");
|
||||
if (catt) catt.value = "";
|
||||
createModal.classList.remove("f7s-open");
|
||||
};
|
||||
|
||||
createModal.addEventListener("click", (ev) => {
|
||||
if (ev.target === createModal) {
|
||||
document.getElementById("close-modal-btn").click();
|
||||
}
|
||||
});
|
||||
|
||||
chatModal.addEventListener("click", (ev) => {
|
||||
if (ev.target === chatModal) {
|
||||
closeChatModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("submit-ticket-btn").onclick = async () => {
|
||||
try {
|
||||
const subject = document.getElementById("subject-input").value.trim();
|
||||
const body = document.getElementById("body-input").value.trim();
|
||||
if (!subject || !body) {
|
||||
showError("Тема и обращение обязательны");
|
||||
return;
|
||||
}
|
||||
const result = await submitTicketWithRetry({
|
||||
server_address: serverAddress,
|
||||
username,
|
||||
subject,
|
||||
body: TICKET_CREATE_BODY_PLACEHOLDER,
|
||||
});
|
||||
const ticketNo = result.ticket_number;
|
||||
let createdMsg;
|
||||
try {
|
||||
createdMsg = await postMessageToTicket(ticketNo, body);
|
||||
} catch (e) {
|
||||
showError(
|
||||
e.message ||
|
||||
`Токен ${ticketNo} создан, но текст обращения не попал в чат. Откройте тикет и напишите сообщение вручную.`
|
||||
);
|
||||
createModal.classList.remove("f7s-open");
|
||||
document.getElementById("subject-input").value = "";
|
||||
document.getElementById("body-input").value = "";
|
||||
const cattFail = document.getElementById("create-ticket-attachment-input");
|
||||
if (cattFail) cattFail.value = "";
|
||||
await fetchTicketsFromNetwork();
|
||||
return;
|
||||
}
|
||||
const firstMessageId = createdMsg.id ?? createdMsg.message_id;
|
||||
const createAtt = document.getElementById("create-ticket-attachment-input");
|
||||
const createFiles = createAtt && createAtt.files ? Array.from(createAtt.files) : [];
|
||||
if (createFiles.length > 0) {
|
||||
if (firstMessageId == null) {
|
||||
showError(`Токен ${ticketNo} создан, но сервер не вернул id сообщения — вложения не отправлены.`);
|
||||
createAtt.value = "";
|
||||
} else {
|
||||
try {
|
||||
for (const f of createFiles) {
|
||||
await uploadAttachmentForMessage(ticketNo, firstMessageId, f);
|
||||
}
|
||||
} catch (attErr) {
|
||||
showError(
|
||||
attErr.message ||
|
||||
`Токен ${ticketNo} создан, текст в чате есть, но не все вложения удалось отправить.`
|
||||
);
|
||||
}
|
||||
}
|
||||
createAtt.value = "";
|
||||
}
|
||||
createModal.classList.remove("f7s-open");
|
||||
document.getElementById("subject-input").value = "";
|
||||
document.getElementById("body-input").value = "";
|
||||
showError(`Токен создан: ${ticketNo}`);
|
||||
await fetchTicketsFromNetwork();
|
||||
void openTicketChat(ticketNo);
|
||||
} catch (e) {
|
||||
showError(e.message || "Ошибка отправки");
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById("send-message-btn").onclick = async () => {
|
||||
if (!state.currentTicket) {
|
||||
showChatError("Сначала выберите токен");
|
||||
return;
|
||||
}
|
||||
const text = document.getElementById("message-input").value.trim();
|
||||
if (!text) return;
|
||||
showChatError("");
|
||||
let created;
|
||||
try {
|
||||
created = await postMessageToTicket(state.currentTicket, text);
|
||||
} catch (e) {
|
||||
showChatError(e.message || "Не удалось отправить сообщение");
|
||||
return;
|
||||
}
|
||||
const messageId = created.id ?? created.message_id;
|
||||
const fileInput = document.getElementById("attachment-input");
|
||||
if (fileInput.files && fileInput.files[0]) {
|
||||
if (messageId == null) {
|
||||
showChatError("Сообщение создано, но сервер не вернул id — вложение не отправлено");
|
||||
fileInput.value = "";
|
||||
} else {
|
||||
try {
|
||||
await uploadAttachmentForMessage(state.currentTicket, messageId, fileInput.files[0]);
|
||||
} catch (e) {
|
||||
showChatError(e.message || "Сообщение отправлено, но вложение не принято");
|
||||
}
|
||||
fileInput.value = "";
|
||||
}
|
||||
}
|
||||
document.getElementById("message-input").value = "";
|
||||
await loadMessages(true, false);
|
||||
};
|
||||
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.hidden) return;
|
||||
void fetchTicketsFromNetwork().catch(() => {});
|
||||
if (isChatOpen() && state.currentTicket) {
|
||||
void loadMessages(false, false);
|
||||
}
|
||||
});
|
||||
|
||||
scheduleTicketsBootstrap();
|
||||
})();
|
||||
Reference in New Issue
Block a user