diff --git a/backend/communications/api/viewsets.py b/backend/communications/api/viewsets.py index 4ed16e03..24a21c35 100644 --- a/backend/communications/api/viewsets.py +++ b/backend/communications/api/viewsets.py @@ -815,7 +815,10 @@ class ChatViewSet(viewsets.ModelViewSet): # Если все еще нет around_id - возвращаем последние сообщения if not around_id: fallback_limit = before_limit + after_limit - messages = list(queryset.order_by('-created_at')[:fallback_limit]) + messages = list(queryset.order_by('-created_at', '-id')[:fallback_limit + 1]) + has_more_before = len(messages) > fallback_limit + if has_more_before: + messages = messages[:fallback_limit] messages.reverse() # Автоотметка: последнее ЗАГРУЖЕННОЕ становится last_read @@ -827,7 +830,9 @@ class ChatViewSet(viewsets.ModelViewSet): 'messages': [serialize_message(m) for m in messages], 'messages_count': len(messages), 'anchor_id': None, - 'anchor_index': 0 + 'anchor_index': 0, + 'has_more_before': has_more_before, + 'has_more_after': False, }) # Определяем, это ID сообщения или timestamp @@ -872,7 +877,10 @@ class ChatViewSet(viewsets.ModelViewSet): if not anchor_msg: # Если не найдено - возвращаем последние сообщения fallback_limit = before_limit + after_limit - messages = list(queryset.order_by('-created_at')[:fallback_limit]) + messages = list(queryset.order_by('-created_at', '-id')[:fallback_limit + 1]) + has_more_before = len(messages) > fallback_limit + if has_more_before: + messages = messages[:fallback_limit] messages.reverse() # Автоотметка: последнее ЗАГРУЖЕННОЕ становится last_read @@ -884,7 +892,9 @@ class ChatViewSet(viewsets.ModelViewSet): 'messages': [serialize_message(m) for m in messages], 'messages_count': len(messages), 'anchor_id': None, - 'anchor_index': 0 + 'anchor_index': 0, + 'has_more_before': has_more_before, + 'has_more_after': False, }) # Асимметричная загрузка: больше контекста, меньше новых diff --git a/backend/tests/api/v1/communications/test_communications_api.py b/backend/tests/api/v1/communications/test_communications_api.py index cf4dc03f..e3051087 100644 --- a/backend/tests/api/v1/communications/test_communications_api.py +++ b/backend/tests/api/v1/communications/test_communications_api.py @@ -286,6 +286,36 @@ class TestChatViewSet: assert "messages" in response.data assert response.data["messages_count"] > 0 + def test_messages_around_without_anchor_reports_more_older( + self, auth_client, private_chat, user1 + ): + """Fallback messages-around без якоря должен сообщать, что вверх есть история.""" + for i in range(40): + Message.objects.create(chat=private_chat, author=user1, content=f"Msg {i}") + + url = f"/api/v1/communications/chats/{private_chat.pk}/messages-around/" + response = auth_client.get(url, {"limit": 30}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["messages_count"] == 30 + assert response.data["has_more_before"] is True + assert response.data["has_more_after"] is False + + def test_messages_around_with_missing_anchor_reports_more_older( + self, auth_client, private_chat, user1 + ): + """Fallback messages-around с отсутствующим around_id не должен скрывать старую историю.""" + for i in range(40): + Message.objects.create(chat=private_chat, author=user1, content=f"Msg {i}") + + url = f"/api/v1/communications/chats/{private_chat.pk}/messages-around/" + response = auth_client.get(url, {"around_id": 999999, "limit": 30}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["messages_count"] == 30 + assert response.data["has_more_before"] is True + assert response.data["has_more_after"] is False + def test_messages_pagination_before(self, auth_client, private_chat, user1): """Пагинация сообщений (before)""" messages = [ diff --git a/frontend/src/app/messages/[chatId]/page.tsx b/frontend/src/app/messages/[chatId]/page.tsx index 573b92da..3a3e050c 100644 --- a/frontend/src/app/messages/[chatId]/page.tsx +++ b/frontend/src/app/messages/[chatId]/page.tsx @@ -441,6 +441,47 @@ function MessageDialogPageContent() { return () => viewport.removeEventListener("scroll", onScroll); }, [cm, markRead, scroll]); + useEffect(() => { + if (cm.messagesLoading || cm.loadingOlder || displayMessages.length === 0) { + return; + } + + if (!cm.hasMoreOlder && !cm.allowOneOlderProbe) { + return; + } + + const component = messagesViewportRef.current; + const viewport = component?.containerRef?.current; + if (!viewport) { + return; + } + + let cancelled = false; + + const frameId = window.requestAnimationFrame(() => { + if (cancelled) { + return; + } + + const hasScrollableOverflow = viewport.scrollHeight > viewport.clientHeight + 8; + if (!hasScrollableOverflow) { + void cm.loadOlderMessages(); + } + }); + + return () => { + cancelled = true; + window.cancelAnimationFrame(frameId); + }; + }, [ + cm.allowOneOlderProbe, + cm.hasMoreOlder, + cm.loadingOlder, + cm.loadOlderMessages, + cm.messagesLoading, + displayMessages.length, + ]); + /* ── fallback sync ── */ useChatFallbackSync({ chatId: chatId || null,