From 81beafda296f892fd1bd04f063d38705260269e4 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 14 May 2026 16:56:40 +0300 Subject: [PATCH] =?UTF-8?q?f7support:=20=D1=82=D0=BE=D1=87=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B5=D0=BF=D1=80=D0=BE=D1=87=D0=B8=D1=82=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BF=D0=BE=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=D0=B4=D0=BD=D0=B5=D0=BC=D1=83=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8E=20(/messages)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Список тикетов часто не меняет preview/activity при ответе оператора. Добавлен опрос сигнатуры последнего сообщения пачками по незакрытым тикетам (кроме открытого чата), сравнение id+created_at. Baseline при закрытии чата (await). Версия 0.1.8. --- appinfo/info.xml | 2 +- js/main.js | 86 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 56c822a..f23172f 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -4,7 +4,7 @@ f7support Support ticket client for F7cloud (F7cloud-compatible) f7support client app for creating and viewing support tickets. - 0.1.7 + 0.1.8 AGPL f7support team F7Support diff --git a/js/main.js b/js/main.js index 5473ce3..a868920 100644 --- a/js/main.js +++ b/js/main.js @@ -76,6 +76,12 @@ pendingFile: null, /** @type {Record} последний известный «отпечаток» карточки по номеру тикета (для точки без has_unread с API) */ ticketSeenFingerprint: {}, + /** @type {Record} сигнатура последнего сообщения (id+время) по тикету — список тикетов часто не меняет preview/activity */ + ticketLastMsgSig: Object.create(null), + /** @type {Record} новое сообщение по сравнению с последним опросом /messages */ + ticketUnreadByLastMsg: Object.create(null), + /** Смещение пачки опросов /messages (не грузим все тикеты за один раз) */ + msgSigPollIndex: 0, }; function clientIdentityHeaders() { @@ -271,6 +277,7 @@ const node = messageList.querySelector(`[data-f7-msg-id="${id}"]`); if (node) hydrateClientAttachments(node); scrollChatToBottom(); + syncCurrentTicketLastMsgSigFromState(); if (clientReadReceiptsEnabled && msg.author_role === "support" && state.currentTicket) { markClientTicketRead(state.currentTicket).catch(() => {}); } @@ -294,6 +301,7 @@ if (!res.ok) return; const row = state.tickets.find((x) => x.ticket_number === ticketNumber); if (row) row.has_unread = false; + delete state.ticketUnreadByLastMsg[String(ticketNumber)]; renderTickets(); reapplyActiveTicketCard(ticketNumber); } @@ -716,10 +724,71 @@ window.setTimeout(run, 320); } + function lastMessageSignatureFromList(msgs) { + if (!Array.isArray(msgs) || msgs.length === 0) return null; + const last = msgs[msgs.length - 1]; + return `${last.id ?? ""}\u0002${last.created_at ?? ""}`; + } + + function syncCurrentTicketLastMsgSigFromState() { + if (!state.currentTicket) return; + const tn = String(state.currentTicket); + const sig = lastMessageSignatureFromList(state.messages); + if (sig == null) return; + state.ticketLastMsgSig[tn] = sig; + delete state.ticketUnreadByLastMsg[tn]; + } + + async function refreshLastMsgSigOne(tn, opts) { + const forceBaseline = opts && opts.forceBaseline === true; + const key = String(tn); + try { + const r = await fetch(`${apiBase}/api/client/tickets/${encodeURIComponent(key)}/messages`, { + headers: clientIdentityHeaders(), + }); + if (!r.ok) return; + const msgs = await r.json(); + const sig = lastMessageSignatureFromList(msgs); + if (sig == null) return; + const prev = state.ticketLastMsgSig[key]; + state.ticketLastMsgSig[key] = sig; + if (!forceBaseline && prev != null && sig !== prev) { + state.ticketUnreadByLastMsg[key] = true; + } + } catch (_) {} + } + + async function refreshLastMessageSignaturesStaggered() { + const cur = state.chatModalOpen && state.currentTicket != null ? String(state.currentTicket) : null; + const openTickets = state.tickets.filter((t) => { + const tn = String(t.ticket_number); + if (cur && tn === cur) return false; + if (String(t.status || "") === "Закрыт") return false; + return true; + }); + if (!openTickets.length) return; + const BATCH = 6; + const n = openTickets.length; + const start = state.msgSigPollIndex % n; + const chunk = []; + for (let i = 0; i < Math.min(BATCH, n); i++) { + chunk.push(String(openTickets[(start + i) % n].ticket_number)); + } + state.msgSigPollIndex = (state.msgSigPollIndex + chunk.length) % n; + await Promise.all(chunk.map((tn) => refreshLastMsgSigOne(tn, null))); + } + function ticketBoardFingerprint(t) { const act = String(t.activity_at || t.updated_at || t.last_activity_at || t.created_at || ""); const prev = t.preview_text != null ? String(t.preview_text) : ""; - return `${act}\u0001${prev}`; + const cnt = + t.messages_count ?? + t.message_count ?? + t.msg_count ?? + t.replies_count ?? + t.reply_count ?? + ""; + return `${act}\u0001${prev}\u0001${cnt}`; } /** Для открытого чата учитываем последнее сообщение, чтобы отпечаток не отставал от превью в списке. */ @@ -756,7 +825,9 @@ } function ticketHasUnread(ticket) { + const tn = String(ticket.ticket_number); if (ticket && ticket._f7_board_unread) return true; + if (state.ticketUnreadByLastMsg[tn]) return true; const raw = ticket?.has_unread ?? ticket?.hasUnread ?? @@ -843,11 +914,13 @@ setModalVisible(chatModal, true); } - function closeChatModal() { + async function closeChatModal() { const closingTn = state.currentTicket; if (closingTn != null) { const row = state.tickets.find((x) => String(x.ticket_number) === String(closingTn)); if (row) state.ticketSeenFingerprint[String(closingTn)] = ticketBoardFingerprint(row); + delete state.ticketUnreadByLastMsg[String(closingTn)]; + await refreshLastMsgSigOne(String(closingTn), { forceBaseline: true }); } disconnectTicketSocket(); revokeAttachmentUrls(); @@ -861,7 +934,7 @@ setModalVisible(chatModal, false); showError(""); document.querySelectorAll(".f7-ticket-card--active").forEach((n) => n.classList.remove("f7-ticket-card--active")); - fetchTickets().catch(() => {}); + await fetchTickets().catch(() => {}); } async function fetchTickets() { @@ -873,6 +946,7 @@ }); if (!response.ok) throw new Error("Не удалось загрузить список обращений"); state.tickets = await response.json(); + await refreshLastMessageSignaturesStaggered(); updateBoardUnreadFingerprints(); renderTickets(); } @@ -942,6 +1016,7 @@ const card = document.querySelector(`.f7-ticket-card[data-ticket-number="${tnSel}"]`); if (card) card.classList.add("f7-ticket-card--active"); openChatModal(ticket); + delete state.ticketUnreadByLastMsg[String(ticketNumber)]; try { await fetchMessages(); await fetchTickets(); @@ -954,7 +1029,7 @@ } catch (e) { showError(e.message || "Не удалось загрузить сообщения"); disconnectTicketSocket(); - closeChatModal(); + await closeChatModal(); } } @@ -982,6 +1057,7 @@ }); if (!response.ok) throw new Error("Не удалось загрузить сообщения"); state.messages = await response.json(); + syncCurrentTicketLastMsgSigFromState(); renderMessages(); } @@ -1088,7 +1164,7 @@ }; document.getElementById("chat-modal-close-btn").onclick = () => { - closeChatModal(); + void closeChatModal(); }; document.getElementById("attachment-clip-btn").onclick = () => {