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:
@@ -24,7 +24,7 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Send, User, AtSign, Reply, X, Edit, Check, CheckCheck, MoreVertical, Bot, Link as LinkIcon, Clock, BarChart2, Paperclip, Loader2 } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { isPWAInstalled } from '@/lib/pwa';
|
||||
import { isPWAInstalled, reportActiveChat, clearActiveChat, setActiveChatInSw, clearActiveChatInSw } from '@/lib/pwa';
|
||||
import { PendingAttachmentList, AttachmentDisplay } from '@/components/chat/ChatAttachments';
|
||||
import type { ChatAttachment } from '@/components/chat/ChatAttachments';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
@@ -238,6 +238,7 @@ const TaskChat = ({ taskId }: TaskChatProps) => {
|
||||
// Мгновенно добавляем новое сообщение если оно относится к текущей задаче
|
||||
// Проверяем author для сообщений пользователей, bot для сообщений ботов
|
||||
const isFromCurrentUser = data.message.author && user && data.message.author.id === user.id;
|
||||
const isVisible = typeof document !== 'undefined' && document.visibilityState === 'visible';
|
||||
|
||||
if (data.taskId === taskId && !isFromCurrentUser) {
|
||||
// Обновляем кэш с новым сообщением
|
||||
@@ -256,6 +257,11 @@ const TaskChat = ({ taskId }: TaskChatProps) => {
|
||||
messages: [...currentMessages, data.message]
|
||||
};
|
||||
});
|
||||
|
||||
// Автоотметка прочтения для видимого открытого чата.
|
||||
if (isVisible && data.message.id > 0) {
|
||||
markAsReadMutation.mutate(data.message.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
onReactionUpdated: (data) => {
|
||||
@@ -508,6 +514,29 @@ const TaskChat = ({ taskId }: TaskChatProps) => {
|
||||
}
|
||||
}, [messages.length, isLoading, user, hasMarkedInitialMessages]);
|
||||
|
||||
// Сообщаем серверу и SW, что этот task-чат открыт, чтобы подавить push.
|
||||
useEffect(() => {
|
||||
if (!taskId || taskId <= 0) return;
|
||||
|
||||
const report = (visible: boolean) => {
|
||||
reportActiveChat('task', taskId, visible);
|
||||
setActiveChatInSw('task', taskId, visible);
|
||||
};
|
||||
|
||||
report(document.visibilityState === 'visible');
|
||||
|
||||
const handleVisibility = () => {
|
||||
report(document.visibilityState === 'visible');
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibility);
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibility);
|
||||
clearActiveChat();
|
||||
clearActiveChatInSw();
|
||||
};
|
||||
}, [taskId]);
|
||||
|
||||
// Keep activeQueueIdsRef in sync with IndexedDB whenever the queue changes.
|
||||
// The offline-queue-updated event fires on enqueue (queryClient.ts) and on
|
||||
// successful item removal (useOfflineSync.ts), so this covers all transitions.
|
||||
|
||||
@@ -256,6 +256,46 @@ export async function initWebPush(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Active chat reporting (used to suppress push for open chats) ──────────────
|
||||
|
||||
export async function reportActiveChat(
|
||||
type: 'messenger' | 'task',
|
||||
id: number,
|
||||
visible: boolean
|
||||
): Promise<void> {
|
||||
try {
|
||||
await apiRequest('POST', '/api/devices/active-chat', { type, id, visible });
|
||||
} catch (error) {
|
||||
console.error('[ActiveChat] Failed to report active chat:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearActiveChat(): Promise<void> {
|
||||
try {
|
||||
await apiRequest('DELETE', '/api/devices/active-chat');
|
||||
} catch (error) {
|
||||
console.error('[ActiveChat] Failed to clear active chat:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export function setActiveChatInSw(
|
||||
type: 'messenger' | 'task',
|
||||
id: number,
|
||||
visible: boolean
|
||||
): void {
|
||||
if (!('serviceWorker' in navigator)) return;
|
||||
const controller = navigator.serviceWorker.controller;
|
||||
if (!controller) return;
|
||||
controller.postMessage({ type: 'ACTIVE_CHAT', payload: { type, id, visible } });
|
||||
}
|
||||
|
||||
export function clearActiveChatInSw(): void {
|
||||
if (!('serviceWorker' in navigator)) return;
|
||||
const controller = navigator.serviceWorker.controller;
|
||||
if (!controller) return;
|
||||
controller.postMessage({ type: 'CLEAR_ACTIVE_CHAT' });
|
||||
}
|
||||
|
||||
export function initPWA(): void {
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
dispatchAttachmentQueueUpdated,
|
||||
} from '@/lib/attachmentQueue';
|
||||
import { getAll as getAllQueued } from '@/lib/offlineQueue';
|
||||
import { reportActiveChat, clearActiveChat, setActiveChatInSw, clearActiveChatInSw } from '@/lib/pwa';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { useEvents } from '@/hooks/useEvents';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
@@ -1048,6 +1049,29 @@ export default function ChatPage() {
|
||||
};
|
||||
}, [activeConvId]);
|
||||
|
||||
// Report active chat to server and SW so push is suppressed while this chat is open.
|
||||
useEffect(() => {
|
||||
if (!activeConvId) return;
|
||||
|
||||
const report = (visible: boolean) => {
|
||||
reportActiveChat('messenger', activeConvId, visible);
|
||||
setActiveChatInSw('messenger', activeConvId, visible);
|
||||
};
|
||||
|
||||
report(document.visibilityState === 'visible');
|
||||
|
||||
const handleVisibility = () => {
|
||||
report(document.visibilityState === 'visible');
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibility);
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibility);
|
||||
clearActiveChat();
|
||||
clearActiveChatInSw();
|
||||
};
|
||||
}, [activeConvId]);
|
||||
|
||||
const activeConv = conversations.find(c => c.id === activeConvId) ?? null;
|
||||
|
||||
const { data: msgsData, isLoading: msgsLoading } = useQuery<{ success: boolean; messages: ConvMessage[] }>({
|
||||
|
||||
Reference in New Issue
Block a user