f7support: опрос доски тикетов, скролл чата, непрочитанные
- Периодический опрос списка тикетов (tickets_poll_ms, по умолчанию 3 с), пока вкладка видима. - Параметр конфига tickets_poll_ms (3000–120000 мс) и data-messages-poll-ms в шаблоне. - Надёжный скролл вниз при открытии чата и после загрузки вложений. - Учёт has_unread как true/1/"1"/"true" для индикатора на карточке.
This commit is contained in:
+48
-3
@@ -8,6 +8,11 @@
|
|||||||
const supportWsBaseOverride = (root.dataset.supportWsBase || "").trim();
|
const supportWsBaseOverride = (root.dataset.supportWsBase || "").trim();
|
||||||
/** Включается в NC: `occ config:app:set f7support client_read_receipts --value=1` после выката API `POST .../read`. */
|
/** Включается в NC: `occ config:app:set f7support client_read_receipts --value=1` после выката API `POST .../read`. */
|
||||||
const clientReadReceiptsEnabled = root.dataset.clientReadReceipts === "1";
|
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_EXT = new Set(["jpg", "jpeg", "png", "gif", "webp", "bmp", "tif", "tiff", "heic", "heif"]);
|
||||||
const RASTER_IMAGE_MIME = new Set([
|
const RASTER_IMAGE_MIME = new Set([
|
||||||
@@ -64,6 +69,7 @@
|
|||||||
blobUrls: [],
|
blobUrls: [],
|
||||||
ticketSocket: null,
|
ticketSocket: null,
|
||||||
wsReconnectTimer: null,
|
wsReconnectTimer: null,
|
||||||
|
ticketsPollTimer: null,
|
||||||
pendingFile: null,
|
pendingFile: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -259,7 +265,7 @@
|
|||||||
}
|
}
|
||||||
const node = messageList.querySelector(`[data-f7-msg-id="${id}"]`);
|
const node = messageList.querySelector(`[data-f7-msg-id="${id}"]`);
|
||||||
if (node) hydrateClientAttachments(node);
|
if (node) hydrateClientAttachments(node);
|
||||||
messageList.scrollTop = messageList.scrollHeight;
|
scrollChatToBottom();
|
||||||
if (clientReadReceiptsEnabled && msg.author_role === "support" && state.currentTicket) {
|
if (clientReadReceiptsEnabled && msg.author_role === "support" && state.currentTicket) {
|
||||||
markClientTicketRead(state.currentTicket).catch(() => {});
|
markClientTicketRead(state.currentTicket).catch(() => {});
|
||||||
}
|
}
|
||||||
@@ -688,6 +694,39 @@
|
|||||||
|
|
||||||
bindClientChatDnDOnce();
|
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) {
|
function showError(message) {
|
||||||
errorBox.textContent = message || "";
|
errorBox.textContent = message || "";
|
||||||
}
|
}
|
||||||
@@ -809,7 +848,7 @@
|
|||||||
const preview = escapeHtml(previewRaw);
|
const preview = escapeHtml(previewRaw);
|
||||||
const activity = ticket.activity_at || ticket.created_at;
|
const activity = ticket.activity_at || ticket.created_at;
|
||||||
const time = escapeHtml(formatTicketCardTime(activity));
|
const time = escapeHtml(formatTicketCardTime(activity));
|
||||||
const hasUnread = Boolean(ticket.has_unread);
|
const hasUnread = ticketHasUnread(ticket);
|
||||||
const unread = hasUnread
|
const unread = hasUnread
|
||||||
? '<span class="f7-unread-dot" title="Новое сообщение" aria-label="Новое сообщение"></span>'
|
? '<span class="f7-unread-dot" title="Новое сообщение" aria-label="Новое сообщение"></span>'
|
||||||
: "";
|
: "";
|
||||||
@@ -911,6 +950,7 @@
|
|||||||
img.src = objUrl;
|
img.src = objUrl;
|
||||||
img.alt = "";
|
img.alt = "";
|
||||||
img.loading = "lazy";
|
img.loading = "lazy";
|
||||||
|
img.addEventListener("load", () => scrollChatToBottom(), { once: true });
|
||||||
frame.appendChild(img);
|
frame.appendChild(img);
|
||||||
wrap.replaceChildren(frame);
|
wrap.replaceChildren(frame);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -945,6 +985,7 @@
|
|||||||
revokeAttachmentUrls();
|
revokeAttachmentUrls();
|
||||||
messageList.innerHTML = state.messages.map((m) => messageBlockHtml(m)).join("");
|
messageList.innerHTML = state.messages.map((m) => messageBlockHtml(m)).join("");
|
||||||
hydrateClientAttachments(messageList);
|
hydrateClientAttachments(messageList);
|
||||||
|
scrollChatToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitTicketWithRetry(payload) {
|
async function submitTicketWithRetry(payload) {
|
||||||
@@ -1084,6 +1125,10 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
fetchTickets().catch((e) => showError(e.message));
|
fetchTickets()
|
||||||
|
.catch((e) => showError(e.message))
|
||||||
|
.finally(() => {
|
||||||
|
scheduleTicketsBoardPolling();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -50,6 +50,15 @@ class PageController extends Controller {
|
|||||||
// Отключить вызовы с клиента: occ config:app:set f7support client_read_receipts --value=0
|
// Отключить вызовы с клиента: occ config:app:set f7support client_read_receipts --value=0
|
||||||
$clientReadReceipts = $this->config->getAppValue('f7support', 'client_read_receipts', '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::addStyle('f7support', 'f7support');
|
||||||
Util::addScript('f7support', 'main');
|
Util::addScript('f7support', 'main');
|
||||||
|
|
||||||
@@ -60,6 +69,7 @@ class PageController extends Controller {
|
|||||||
'supportApiOrigin' => $supportApiOrigin,
|
'supportApiOrigin' => $supportApiOrigin,
|
||||||
'supportWsBase' => $supportWsBase,
|
'supportWsBase' => $supportWsBase,
|
||||||
'clientReadReceipts' => $clientReadReceipts,
|
'clientReadReceipts' => $clientReadReceipts,
|
||||||
|
'ticketsPollMs' => (string)$ticketsPollMs,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -5,5 +5,6 @@
|
|||||||
data-server-address="<?php p($_['serverAddress']); ?>"
|
data-server-address="<?php p($_['serverAddress']); ?>"
|
||||||
data-support-api-base="<?php p($_['supportApiBase']); ?>"
|
data-support-api-base="<?php p($_['supportApiBase']); ?>"
|
||||||
data-support-ws-base="<?php p($_['supportWsBase'] ?? ''); ?>"
|
data-support-ws-base="<?php p($_['supportWsBase'] ?? ''); ?>"
|
||||||
data-client-read-receipts="<?php p($_['clientReadReceipts'] ?? '0'); ?>">
|
data-client-read-receipts="<?php p($_['clientReadReceipts'] ?? '0'); ?>"
|
||||||
|
data-messages-poll-ms="<?php p($_['ticketsPollMs'] ?? '3000'); ?>">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user