From 866a64d413b2281a858b386c0683f192f9944fed Mon Sep 17 00:00:00 2001 From: root Date: Thu, 14 May 2026 14:52:02 +0300 Subject: [PATCH] =?UTF-8?q?f7support:=20=D0=BE=D0=BF=D1=80=D0=BE=D1=81=20?= =?UTF-8?q?=D0=B4=D0=BE=D1=81=D0=BA=D0=B8=20=D1=82=D0=B8=D0=BA=D0=B5=D1=82?= =?UTF-8?q?=D0=BE=D0=B2,=20=D1=81=D0=BA=D1=80=D0=BE=D0=BB=D0=BB=20=D1=87?= =?UTF-8?q?=D0=B0=D1=82=D0=B0,=20=D0=BD=D0=B5=D0=BF=D1=80=D0=BE=D1=87?= =?UTF-8?q?=D0=B8=D1=82=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Периодический опрос списка тикетов (tickets_poll_ms, по умолчанию 3 с), пока вкладка видима. - Параметр конфига tickets_poll_ms (3000–120000 мс) и data-messages-poll-ms в шаблоне. - Надёжный скролл вниз при открытии чата и после загрузки вложений. - Учёт has_unread как true/1/"1"/"true" для индикатора на карточке. --- js/main.js | 51 +++++++++++++++++++++++++++++-- lib/Controller/PageController.php | 10 ++++++ templates/main.php | 3 +- 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/js/main.js b/js/main.js index 7f2eb20..1fe6e49 100644 --- a/js/main.js +++ b/js/main.js @@ -8,6 +8,11 @@ const supportWsBaseOverride = (root.dataset.supportWsBase || "").trim(); /** Включается в NC: `occ config:app:set f7support client_read_receipts --value=1` после выката API `POST .../read`. */ const clientReadReceiptsEnabled = root.dataset.clientReadReceipts === "1"; + const ticketsPollIntervalMs = (() => { + const n = parseInt(String(root.dataset.messagesPollMs || "3000"), 10); + if (!Number.isFinite(n)) return 3000; + return Math.min(120000, Math.max(3000, n)); + })(); const RASTER_IMAGE_EXT = new Set(["jpg", "jpeg", "png", "gif", "webp", "bmp", "tif", "tiff", "heic", "heif"]); const RASTER_IMAGE_MIME = new Set([ @@ -64,6 +69,7 @@ blobUrls: [], ticketSocket: null, wsReconnectTimer: null, + ticketsPollTimer: null, pendingFile: null, }; @@ -259,7 +265,7 @@ } const node = messageList.querySelector(`[data-f7-msg-id="${id}"]`); if (node) hydrateClientAttachments(node); - messageList.scrollTop = messageList.scrollHeight; + scrollChatToBottom(); if (clientReadReceiptsEnabled && msg.author_role === "support" && state.currentTicket) { markClientTicketRead(state.currentTicket).catch(() => {}); } @@ -688,6 +694,39 @@ bindClientChatDnDOnce(); + function scrollChatToBottom() { + const ml = document.getElementById("message-list"); + if (!ml) return; + const run = () => { + try { + ml.scrollTop = ml.scrollHeight; + } catch (_) {} + }; + run(); + requestAnimationFrame(() => { + run(); + requestAnimationFrame(run); + }); + window.setTimeout(run, 80); + window.setTimeout(run, 320); + } + + function ticketHasUnread(ticket) { + const v = ticket?.has_unread; + return v === true || v === 1 || v === "1" || v === "true"; + } + + function scheduleTicketsBoardPolling() { + if (state.ticketsPollTimer) { + clearInterval(state.ticketsPollTimer); + state.ticketsPollTimer = null; + } + state.ticketsPollTimer = window.setInterval(() => { + if (document.visibilityState === "hidden") return; + fetchTickets().catch(() => {}); + }, ticketsPollIntervalMs); + } + function showError(message) { errorBox.textContent = message || ""; } @@ -809,7 +848,7 @@ const preview = escapeHtml(previewRaw); const activity = ticket.activity_at || ticket.created_at; const time = escapeHtml(formatTicketCardTime(activity)); - const hasUnread = Boolean(ticket.has_unread); + const hasUnread = ticketHasUnread(ticket); const unread = hasUnread ? '' : ""; @@ -911,6 +950,7 @@ img.src = objUrl; img.alt = ""; img.loading = "lazy"; + img.addEventListener("load", () => scrollChatToBottom(), { once: true }); frame.appendChild(img); wrap.replaceChildren(frame); } catch (_) { @@ -945,6 +985,7 @@ revokeAttachmentUrls(); messageList.innerHTML = state.messages.map((m) => messageBlockHtml(m)).join(""); hydrateClientAttachments(messageList); + scrollChatToBottom(); } async function submitTicketWithRetry(payload) { @@ -1084,6 +1125,10 @@ }; queueMicrotask(() => { - fetchTickets().catch((e) => showError(e.message)); + fetchTickets() + .catch((e) => showError(e.message)) + .finally(() => { + scheduleTicketsBoardPolling(); + }); }); })(); diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index cdcae88..3043b01 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -50,6 +50,15 @@ class PageController extends Controller { // Отключить вызовы с клиента: occ config:app:set f7support client_read_receipts --value=0 $clientReadReceipts = $this->config->getAppValue('f7support', 'client_read_receipts', '0'); + // Интервал опроса списка тикетов (мс), чтобы подтягивать has_unread без WebSocket. occ: f7support tickets_poll_ms + $ticketsPollMs = (int)$this->config->getAppValue('f7support', 'tickets_poll_ms', '3000'); + if ($ticketsPollMs < 3000) { + $ticketsPollMs = 3000; + } + if ($ticketsPollMs > 120000) { + $ticketsPollMs = 120000; + } + Util::addStyle('f7support', 'f7support'); Util::addScript('f7support', 'main'); @@ -60,6 +69,7 @@ class PageController extends Controller { 'supportApiOrigin' => $supportApiOrigin, 'supportWsBase' => $supportWsBase, 'clientReadReceipts' => $clientReadReceipts, + 'ticketsPollMs' => (string)$ticketsPollMs, ]); } } diff --git a/templates/main.php b/templates/main.php index 47fbf5d..1cfa2a8 100644 --- a/templates/main.php +++ b/templates/main.php @@ -5,5 +5,6 @@ data-server-address="" data-support-api-base="" data-support-ws-base="" - data-client-read-receipts=""> + data-client-read-receipts="" + data-messages-poll-ms="">