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

@@ -394,6 +394,88 @@ export class ContentStorage extends DataTablesStorage {
return userIds.filter(id => !onlineUserIds.has(id));
}
async updateUserActiveChat(
userId: number,
organizationId: number,
type: 'messenger' | 'task',
id: number,
visible: boolean
): Promise<void> {
const existing = await db.select().from(userPresence)
.where(and(
eq(userPresence.userId, userId),
eq(userPresence.organizationId, organizationId)
))
.limit(1);
if (existing.length > 0) {
await db.update(userPresence)
.set({
activeChatType: type,
activeChatId: id,
activeChatVisible: visible,
activeChatUpdatedAt: new Date(),
lastSeenAt: new Date(),
})
.where(and(
eq(userPresence.userId, userId),
eq(userPresence.organizationId, organizationId)
));
} else {
await db.insert(userPresence).values({
userId,
organizationId,
isOnline: true,
lastSeenAt: new Date(),
activeDevices: 0,
activeChatType: type,
activeChatId: id,
activeChatVisible: visible,
activeChatUpdatedAt: new Date(),
});
}
}
async clearUserActiveChat(userId: number, organizationId: number): Promise<void> {
await db.update(userPresence)
.set({
activeChatType: null,
activeChatId: null,
activeChatVisible: false,
activeChatUpdatedAt: new Date(),
})
.where(and(
eq(userPresence.userId, userId),
eq(userPresence.organizationId, organizationId)
));
}
async getUserActiveChat(userId: number, organizationId: number): Promise<{
type: 'messenger' | 'task' | null;
id: number | null;
visible: boolean;
updatedAt: Date | null;
} | null> {
const [presence] = await db.select({
activeChatType: userPresence.activeChatType,
activeChatId: userPresence.activeChatId,
activeChatVisible: userPresence.activeChatVisible,
activeChatUpdatedAt: userPresence.activeChatUpdatedAt,
}).from(userPresence)
.where(and(
eq(userPresence.userId, userId),
eq(userPresence.organizationId, organizationId)
));
if (!presence) return null;
return {
type: (presence.activeChatType as 'messenger' | 'task') || null,
id: presence.activeChatId ?? null,
visible: presence.activeChatVisible ?? false,
updatedAt: presence.activeChatUpdatedAt ?? null,
};
}
// Web Push Subscriptions
async registerWebPushSubscription(userId: number, organizationId: number, endpoint: string, subscription: string): Promise<void> {
await db.delete(webPushSubscriptions).where(eq(webPushSubscriptions.endpoint, endpoint));