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:
@@ -13,6 +13,7 @@ import {
|
||||
import { webPushService } from "./web-push.service";
|
||||
import { eq, and, or, isNull, inArray, sql } from "drizzle-orm";
|
||||
import { storage } from "../storage";
|
||||
import { eventBus } from "../routes/shared";
|
||||
|
||||
// Интерфейс для push-уведомления (для отправки через Pushy)
|
||||
export interface PushNotificationPayload {
|
||||
@@ -582,6 +583,32 @@ export class NotificationService {
|
||||
|
||||
// === Push Notification Methods ===
|
||||
|
||||
/**
|
||||
* Проверяет, нужно ли подавить видимый push, потому что пользователь уже
|
||||
* смотрит в соответствующий чат на активной видимой вкладке.
|
||||
*/
|
||||
private async shouldSkipPushForActiveChat(
|
||||
userId: number,
|
||||
organizationId: number,
|
||||
chatType: 'messenger' | 'task',
|
||||
chatId: number
|
||||
): Promise<boolean> {
|
||||
// Подавляем только если у пользователя есть активное SSE-соединение,
|
||||
// т.е. открыто приложение.
|
||||
if (!eventBus.isUserConnected(userId, organizationId)) return false;
|
||||
|
||||
const activeChat = await storage.getUserActiveChat(userId, organizationId);
|
||||
if (!activeChat) return false;
|
||||
if (!activeChat.visible || activeChat.type !== chatType || activeChat.id !== chatId) return false;
|
||||
|
||||
// Игнорируем устаревшие записи (например, вкладка закрылась, но не успела
|
||||
// сообщить об этом).
|
||||
const maxAgeMs = 60 * 1000;
|
||||
if (!activeChat.updatedAt || Date.now() - activeChat.updatedAt.getTime() > maxAgeMs) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавляет push-уведомление в очередь для офлайн-пользователей.
|
||||
* Вызывается после создания in-app уведомления.
|
||||
@@ -596,6 +623,18 @@ export class NotificationService {
|
||||
actions?: Array<{ action: string; title: string }>
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Если пользователь уже смотрит в task-чат — не присылаем видимый push.
|
||||
if (data?.type === 'chat' && data?.taskId) {
|
||||
const taskId = parseInt(data.taskId, 10);
|
||||
if (!isNaN(taskId)) {
|
||||
const shouldSkip = await this.shouldSkipPushForActiveChat(userId, organizationId, 'task', taskId);
|
||||
if (shouldSkip) {
|
||||
console.log(`[PushSuppress] Skipping push for user ${userId}: active task chat ${taskId}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Формируем правильный URL для открытия задачи
|
||||
let url = '/';
|
||||
if (data?.taskId && data?.formId) {
|
||||
@@ -716,6 +755,13 @@ export class NotificationService {
|
||||
// Push-уведомление: пропускаем для заглушённых чатов,
|
||||
// КРОМЕ случая когда пользователь упомянут через @mention
|
||||
if (channels.includes('push') && (!isMuted || isMentioned)) {
|
||||
// Не шлём push, если пользователь уже смотрит в этот мессенджерный чат.
|
||||
const shouldSkip = await this.shouldSkipPushForActiveChat(memberId, orgId, 'messenger', conversationId);
|
||||
if (shouldSkip) {
|
||||
console.log(`[PushSuppress] Skipping messenger push for user ${memberId}: active conv ${conversationId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await webPushService.sendToUser(memberId, orgId, {
|
||||
title: chatName,
|
||||
|
||||
Reference in New Issue
Block a user