From 21e8bb0ad0db755a80436b88915d9a9346c41cf9 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 29 Sep 2025 02:06:20 +0300 Subject: [PATCH] add chat list --- app/controllers/main_controller.py | 31 ++++++++++++++- app/core/models/chat_models.py | 57 +++++++++++++++++++------- app/core/services/chat_service.py | 48 ++++++++++++++++++++++ app/ui/views/chat_list_view.py | 64 ++++++++++++++++++++---------- app/ui/views/yobble_home_view.py | 13 +++++- 5 files changed, 176 insertions(+), 37 deletions(-) create mode 100644 app/core/services/chat_service.py diff --git a/app/controllers/main_controller.py b/app/controllers/main_controller.py index e2534fb..ef063fb 100644 --- a/app/controllers/main_controller.py +++ b/app/controllers/main_controller.py @@ -8,11 +8,15 @@ from threading import Thread import time import asyncio from app.core.database import get_last_login, get_session, set_last_login +# Импортируем сервис чатов +from app.core.services.chat_service import get_private_chats class MainController(QStackedWidget): # Сигнал для показа уведомлений из любого потока # (message, is_error) notification_requested = Signal(str, bool) + # Сигнал для передачи загруженных данных чата в основной поток + chats_loaded = Signal(object) def __init__(self): super().__init__() @@ -49,8 +53,9 @@ class MainController(QStackedWidget): self.yobble_home_view = YobbleHomeView(username=username) - # Подключаем сигнал к слоту в YobbleHomeView + # Подключаем сигналы к слотам в YobbleHomeView self.notification_requested.connect(self.yobble_home_view.show_notification) + self.chats_loaded.connect(self.yobble_home_view.update_chat_list) self.addWidget(self.yobble_home_view) self.setCurrentWidget(self.yobble_home_view) @@ -72,8 +77,32 @@ class MainController(QStackedWidget): # asyncio.run() выполняет async функцию и возвращает её результат asyncio.run(self.yobble_home_view.preload_permissions()) + # Загружаем список чатов + print("[Sync] Загружаем список чатов...") + asyncio.run(self.load_chats(username)) + except Exception as e: error_message = f"Ошибка предзагрузки: {e}" print(f"[Sync] {error_message}") # Отправляем сигнал вместо прямого вызова self.notification_requested.emit(error_message, True) + + async def load_chats(self, username: str): + """ + Загружает список чатов для пользователя. + """ + session = get_session(username) + # print("debug session", session) + # if not session or 'access_token' not in session: + # self.notification_requested.emit("Сессия не найдена, не могу загрузить чаты.", True) + # return + + token = session['access_token'] + success, data = await get_private_chats(token=token, offset=0, limit=50) + + if success: + # Отправляем данные в основной поток через сигнал + self.chats_loaded.emit(data) + else: + # Отправляем ошибку в основной поток через сигнал + self.notification_requested.emit(f"Не удалось загрузить чаты: {data}", True) diff --git a/app/core/models/chat_models.py b/app/core/models/chat_models.py index ff78e32..855398e 100644 --- a/app/core/models/chat_models.py +++ b/app/core/models/chat_models.py @@ -1,20 +1,47 @@ -from pydantic import BaseModel, Field -from typing import Optional, Dict, Any, List, Literal from uuid import UUID from datetime import datetime +from pydantic import BaseModel, Field +from typing import Optional, Literal, List, Any, Dict + + +class MessageForward(BaseModel): + forward_type: Optional[Literal["chat_private_messages", "chat_group_messages", + "chat_public_messages", "reply"]] = Field(None, description="Тип пересылаемого контента") + forward_sender_id: Optional[UUID] = Field(None, description="ID чата, откуда переслано сообщение") + forward_message_id: Optional[int] = Field(None, description="Данные внутренний ид сообщения в forward_chat") + forward_chat_data: Optional[Any] = Field(default=None, description="Данные о чате пересылаемом (беседы и паблики)") + + +class MessageItem(BaseModel): + message_id: int = Field(..., description="внутренний ID сообщения") + message_type: List[Literal["text", "media", "circle", "voice", "system", "forward", + "reply", "poll"]] = Field(..., alias="message_type", description="Типы сообщения") + + forward_metadata: Optional[MessageForward] + + chat_id: UUID = Field(..., description="Чат ID") + sender_id: UUID = Field(..., description="Кто отправил") + sender_data: Optional[Any] = Field(default=None, description="Данные о пользователе") + content: Optional[str] = Field(None, description="Текст сообщения") + media_link: Optional[Any] = Field(None, description="Ссылка на медиа (заглушка)") + is_viewed: bool = Field(..., description="Флаг просмотра") + created_at: datetime = Field(..., description="Дата и время создания сообщения") + updated_at: Optional[datetime] = Field(None, description="Дата и время обновления сообщения") -class LastMessage(BaseModel): - message_id: int - message_type: List[Literal["text", "media", "circle", "voice", "system", "forward", "reply", "poll"]] - context: str - created_at: datetime class PrivateChatListItem(BaseModel): - chat_name: Optional[str] - chat_type: Literal["self", "private"] - chat_id: UUID - chat_data: Optional[Dict[str, Any]] = None - companion_id: Optional[UUID] = None - companion_data: Optional[Dict[str, Any]] = None # Типизируй как нужно - last_message: Optional[LastMessage] = None - created_at: datetime + chat_id: UUID = Field(..., description="ID чата") + chat_type: Literal["self", "private"] = Field(..., description="Тип чата") + chat_data: Optional[Dict[str, Any]] = Field(default=None, description="Данные о чате") + last_message: Optional[MessageItem] = Field(None, description="Последнее сообщение в чате") + created_at: datetime = Field(..., description="Дата создания чата") + + +class PrivateChatListData(BaseModel): + items: List[PrivateChatListItem] + has_more: bool + + +class PrivateChatListResponse(BaseModel): + status: str + data: PrivateChatListData \ No newline at end of file diff --git a/app/core/services/chat_service.py b/app/core/services/chat_service.py new file mode 100644 index 0000000..6278c87 --- /dev/null +++ b/app/core/services/chat_service.py @@ -0,0 +1,48 @@ +import httpx +from app.core import config +from app.core.localizer import localizer +from app.core.models.chat_models import PrivateChatListResponse, PrivateChatListData + +async def get_private_chats(token: str, offset: int = 0, limit: int = 20): + """ + Получает список приватных чатов пользователя. + + :param token: Токен доступа пользователя + :param offset: Смещение для пагинации + :param limit: Количество чатов для загрузки + :return: Кортеж (успех: bool, данные: PrivateChatListData | str) + """ + # TODO: Добавить логику обновления токена, как в auth_service.py + url = f"{config.BASE_URL}/v1/chat/private/list" + headers = {"Authorization": f"Bearer {token}"} + params = {"offset": offset, "limit": limit} + + try: + async with httpx.AsyncClient(http2=True) as client: + response = await client.get(url, headers=headers, params=params) + print("response.status_code", response.status_code) + + if response.status_code == 200: + data = response.json() + if data.get("status") == "fine": + # Используем Pydantic модель для парсинга ответа + response_model = PrivateChatListResponse(**data) + print("response_model.data", response_model.data) + return True, response_model.data + else: + return False, data.get("detail", localizer.translate("Неизвестная ошибка ответа")) + + elif response.status_code in [401, 403]: + error_data = response.json() + return False, error_data.get("detail", localizer.translate("Ошибка аутентификации или авторизации")) + + elif response.status_code == 422: + return False, localizer.translate("Некорректные параметры запроса") + + else: + return False, f"{localizer.translate('Ошибка сервера')}: {response.status_code}" + + except httpx.RequestError as e: + return False, f"{localizer.translate('Ошибка сети')}: {e}" + except Exception as e: + return False, f"{localizer.translate('Произошла ошибка')}: {e}" diff --git a/app/ui/views/chat_list_view.py b/app/ui/views/chat_list_view.py index 2b2c871..905fd35 100644 --- a/app/ui/views/chat_list_view.py +++ b/app/ui/views/chat_list_view.py @@ -1,35 +1,59 @@ from PySide6.QtWidgets import QWidget, QListWidget, QVBoxLayout, QLabel, QListWidgetItem +from PySide6.QtCore import Qt +from typing import List from app.core.models.chat_models import PrivateChatListItem class ChatListView(QWidget): - def __init__(self, username, chat_items: list[PrivateChatListItem]): + def __init__(self): super().__init__() - self.setWindowTitle(f"Чаты — {username}") - self.setMinimumSize(400, 500) - - self.chat_items = chat_items self.init_ui() def init_ui(self): - layout = QVBoxLayout() - - self.label = QLabel("Список чатов:") - layout.addWidget(self.label) + """Инициализирует пользовательский интерфейс.""" + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) self.chat_list = QListWidget() - self.render_chat_items() + self.chat_list.setStyleSheet("QListWidget { border: none; }") layout.addWidget(self.chat_list) - self.setLayout(layout) + # Изначальное состояние + self.show_placeholder_message("Загрузка чатов...") - def render_chat_items(self): + def show_placeholder_message(self, text): + """Очищает список и показывает одно сообщение (например, "Загрузка..." или "Чатов нет").""" self.chat_list.clear() - for chat in self.chat_items: - companion_name = chat.companion_data.get("username", "Без имени") if chat.companion_data else "Неизвестный" - last_msg = chat.last_message.context if chat.last_message else "Нет сообщений" - item_text = f"{companion_name} — {last_msg}" - self.chat_list.addItem(QListWidgetItem(item_text)) + item = QListWidgetItem(text) + item.setTextAlignment(Qt.AlignCenter) + self.chat_list.addItem(item) - def update_chat_items(self, new_items: list[PrivateChatListItem]): - self.chat_items = new_items - self.render_chat_items() + def populate_chats(self, chat_items: List[PrivateChatListItem]): + """ + Заполняет список чатов данными, полученными от сервера. + """ + self.chat_list.clear() + + if not chat_items: + self.show_placeholder_message("У вас пока нет чатов") + return + + for chat in chat_items: + # Определяем имя собеседника + if chat.chat_type == "self": + companion_name = "Избранное" + elif chat.chat_data and 'login' in chat.chat_data: + companion_name = chat.chat_data['login'] + else: + companion_name = "Неизвестный" + + # Получаем текст последнего сообщения + if chat.last_message and chat.last_message.content: + last_msg = chat.last_message.content + else: + last_msg = "Нет сообщений" + + # Создаем кастомный виджет для элемента списка (можно будет улучшить) + # Пока просто текстом + item_text = f"{companion_name}\n{last_msg}" + list_item = QListWidgetItem(item_text) + self.chat_list.addItem(list_item) diff --git a/app/ui/views/yobble_home_view.py b/app/ui/views/yobble_home_view.py index 91c49e7..29c1a92 100644 --- a/app/ui/views/yobble_home_view.py +++ b/app/ui/views/yobble_home_view.py @@ -11,6 +11,7 @@ from PySide6.QtGui import QColor from app.core.theme import theme_manager from app.core.dialogs import show_themed_messagebox from app.ui.views.side_menu_view import SideMenuView +from app.ui.views.chat_list_view import ChatListView from app.core.services.auth_service import get_user_role from app.core.database import get_current_access_token from app.core.localizer import localizer @@ -366,11 +367,21 @@ class YobbleHomeView(QWidget): self.content_stack.addWidget(self.music_label) # Чаты - self.content_stack.addWidget(QLabel("Контент Чатов")) + self.chat_list_view = ChatListView() + self.content_stack.addWidget(self.chat_list_view) # Профиль self.content_stack.addWidget(QLabel("Контент Профиля")) + def update_chat_list(self, chat_data): + """ + Слот для обновления списка чатов. + Получает данные из сигнала контроллера. + """ + print(f"[UI] Получены данные для обновления чатов: {len(chat_data.items)} элементов.") + # Передаем данные в виджет списка чатов для отображения + self.chat_list_view.populate_chats(chat_data.items) + def on_tab_button_clicked(self, index): """Обрабатывает нажатие на кнопку вкладки, проверяя права доступа.""" if index in self.REQUIRED_PERMISSIONS: