fix(chat): подавлять push-уведомления при открытом активном чате и автоотметка прочтения в task chat

- Добавлены active_chat_* поля в user_presence и endpoint /api/devices/active-chat

- Сервер не шлёт Web Push/Pushy, если получатель смотрит в тот же чат на видимой вкладке

- Клиент сообщает серверу и Service Worker об активном чате из Chat.tsx и TaskChat.tsx

- TaskChat: входящие сообщения автоматически отмечаются прочитанными при видимой вкладке

- SW: подавляет foreground/system push, если активный чат совпадает
This commit is contained in:
2026-07-16 18:59:00 +03:00
parent 63c83a059d
commit 7672dd98df
9 changed files with 317 additions and 1 deletions

View File

@@ -6,6 +6,11 @@ const STATIC_ASSETS = [
'/favicon.svg'
];
// Активный чат, о котором сообщает клиент через postMessage.
// Используется для подавления push-уведомлений, когда пользователь уже
// смотрит в соответствующий чат.
let activeChat = null; // { type: 'messenger'|'task', id: number, visible: boolean, updatedAt: number }
// ── IndexedDB helpers (mirrors offlineQueue.ts schema, must stay in sync) ────
// Schema history:
// v1 – `queue` store (keyPath: id, index: enqueuedAt)
@@ -480,6 +485,26 @@ async function sendToForegroundClients(payload) {
}
}
function shouldSuppressPushForActiveChat(payload) {
if (!activeChat || !activeChat.visible) return false;
// Игнорируем устаревшие записи.
const maxAgeMs = 60 * 1000;
if (!activeChat.updatedAt || Date.now() - activeChat.updatedAt > maxAgeMs) return false;
const nestedType = payload.data?.type;
if (nestedType === 'messenger' && activeChat.type === 'messenger') {
return parseInt(payload.data?.conversationId, 10) === activeChat.id;
}
if ((nestedType === 'chat' || payload.type === 'chat') && activeChat.type === 'task') {
return parseInt(payload.data?.taskId || payload.taskId, 10) === activeChat.id;
}
return false;
}
self.addEventListener('push', (event) => {
console.log('[SW] Push notification received');
@@ -509,6 +534,12 @@ self.addEventListener('push', (event) => {
return;
}
// Подавляем push, если пользователь уже смотрит в этот чат на активной вкладке.
if (shouldSuppressPushForActiveChat(data)) {
console.log('[SW] Push suppressed for active chat:', activeChat);
return;
}
event.waitUntil(
hasVisibleClient().then((visible) => {
if (visible) {
@@ -620,5 +651,21 @@ self.addEventListener('pushsubscriptionchange', (event) => {
self.addEventListener('message', (event) => {
if (event.data?.type === 'SKIP_WAITING') {
self.skipWaiting();
return;
}
if (event.data?.type === 'ACTIVE_CHAT') {
const payload = event.data?.payload || {};
activeChat = {
type: payload.type,
id: payload.id,
visible: !!payload.visible,
updatedAt: Date.now(),
};
return;
}
if (event.data?.type === 'CLEAR_ACTIVE_CHAT') {
activeChat = null;
}
});