From ccad715e365169529c7b5bc4f07602b98e62cd22 Mon Sep 17 00:00:00 2001 From: lizerk Date: Thu, 16 Apr 2026 12:07:53 +0300 Subject: [PATCH] chore: snapshot current messages and notifications work --- .../services/chat_notifications.py | 1 + backend/tests/realtime/test_user_consumer.py | 49 +++++++++++++++++++ frontend/src/app/globals.css | 18 +++++++ frontend/src/app/messages/[chatId]/page.tsx | 23 +++++++++ frontend/src/app/messages/page.tsx | 4 +- 5 files changed, 93 insertions(+), 2 deletions(-) diff --git a/backend/communications/services/chat_notifications.py b/backend/communications/services/chat_notifications.py index 4798c3e0..2d5c5d38 100644 --- a/backend/communications/services/chat_notifications.py +++ b/backend/communications/services/chat_notifications.py @@ -24,6 +24,7 @@ class ChatNotificationReadService: NotificationVerbs.MENTION, NotificationVerbs.REPLY, NotificationVerbs.ADDED_TO_CHAT, + NotificationVerbs.COMMENTED, ) def mark_chat_notifications_as_read( diff --git a/backend/tests/realtime/test_user_consumer.py b/backend/tests/realtime/test_user_consumer.py index a98cb97e..7a9ea2f4 100644 --- a/backend/tests/realtime/test_user_consumer.py +++ b/backend/tests/realtime/test_user_consumer.py @@ -150,6 +150,55 @@ class TestUserConsumerChatManagement: assert unrelated_notification.unread is True await communicator.disconnect() + + async def test_open_chat_marks_related_commented_notifications_as_read(self, ws_communicator, user, test_chat): + """Открытие comments-чата должно читать уведомления verb=commented этого чата.""" + related_notification = await database_sync_to_async(Notification.objects.create)( + recipient=user, + verb='commented', + description='Department chat comment notification', + action_url=f'/messages/{test_chat.id}', + data={'chat_id': test_chat.id, 'message_id': 12, 'object_type': 'Department'}, + ) + unrelated_notification = await database_sync_to_async(Notification.objects.create)( + recipient=user, + verb='commented', + description='Other comments chat notification', + action_url='/messages/999', + data={'chat_id': 999, 'message_id': 13, 'object_type': 'Department'}, + ) + + communicator = await ws_communicator(user=user) + + connected, _ = await communicator.connect() + assert connected + + await communicator.send_json_to({ + 'action': 'open_chat', + 'chat_id': test_chat.id, + 'load_history': False, + }) + + response = await communicator.receive_json_from(timeout=5) + assert response['type'] == 'chat_opened' + assert response['chat_id'] == test_chat.id + + read_all_event = await communicator.receive_json_from(timeout=5) + assert read_all_event['type'] == 'notifications_read_all' + assert related_notification.id in read_all_event['notification_ids'] + assert unrelated_notification.id not in read_all_event['notification_ids'] + + count_event = await communicator.receive_json_from(timeout=5) + assert count_event['type'] == 'unread_count' + assert count_event['count'] == 1 + + related_notification = await database_sync_to_async(Notification.objects.get)(id=related_notification.id) + unrelated_notification = await database_sync_to_async(Notification.objects.get)(id=unrelated_notification.id) + + assert related_notification.unread is False + assert unrelated_notification.unread is True + + await communicator.disconnect() async def test_open_nonexistent_chat(self, ws_communicator, user): """Попытка открыть несуществующий чат возвращает ошибку.""" diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 6f1386ba..8f642d57 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -378,6 +378,24 @@ html[data-bs-theme="dark"] .app-input::-webkit-calendar-picker-indicator { color: #ffffff; } +.chat-list-unread-badge { + min-height: 1.35rem; + border: 2px solid var(--surface-primary); + box-shadow: 0 4px 10px color-mix(in srgb, #ef4444 22%, transparent); + letter-spacing: -0.01em; +} + +.chat-unread-divider { + color: var(--accent-primary-strong); +} + +.chat-unread-divider__label { + border: 1px solid color-mix(in srgb, var(--accent-primary) 20%, var(--border-subtle)); + background: color-mix(in srgb, var(--accent-primary) 10%, var(--surface-primary)); + color: var(--accent-primary-strong); + box-shadow: 0 6px 16px color-mix(in srgb, var(--accent-primary) 10%, transparent); +} + .app-action-primary { border: 1px solid transparent; background: var(--accent-primary); diff --git a/frontend/src/app/messages/[chatId]/page.tsx b/frontend/src/app/messages/[chatId]/page.tsx index 7802ec32..8166cdc8 100644 --- a/frontend/src/app/messages/[chatId]/page.tsx +++ b/frontend/src/app/messages/[chatId]/page.tsx @@ -135,6 +135,19 @@ function MessageDialogPageContent() { /* ── derived state ── */ const displayMessages = useMemo(() => mergeDisplayMessages(cm.messages, cm.pendingMessages), [cm.messages, cm.pendingMessages]); const messagesById = useMemo(() => new Map(displayMessages.map(m => [m.id, m])), [displayMessages]); + const unreadDividerMessageId = useMemo(() => { + if (cm.newMessagesBelowCount <= 0) { + return null; + } + + const unreadBelowIds = cm.unreadBelowIdsRef.current; + if (!unreadBelowIds || unreadBelowIds.size === 0) { + return null; + } + + const firstUnreadIncoming = displayMessages.find((message) => unreadBelowIds.has(message.id)); + return firstUnreadIncoming?.id ?? null; + }, [cm.newMessagesBelowCount, cm.unreadBelowIdsRef, displayMessages]); /** * Badge count: take the maximum of the server-provided unread_count @@ -1057,6 +1070,7 @@ function MessageDialogPageContent() { const prevDay = index > 0 ? getMessageDayKey(displayMessages[index - 1]) : null; const curDate = getMessageDate(message); const showDayDivider = Boolean(curDate && curDay !== prevDay); + const showUnreadDivider = unreadDividerMessageId === message.id; return ( @@ -1067,6 +1081,15 @@ function MessageDialogPageContent() { ) : null} + {showUnreadDivider ? ( +
+ + + Непрочитанные сообщения + + +
+ ) : null}
-
+
{chatAvatar ? ( {/* Счетчик непрочитанных */} {(chat.unread_count ?? 0) > 0 ? ( - + {chat.unread_count! > 99 ? '99+' : chat.unread_count} ) : null}