add chat list
This commit is contained in:
		
							parent
							
								
									99029541d3
								
							
						
					
					
						commit
						21e8bb0ad0
					
				@ -8,11 +8,15 @@ from threading import Thread
 | 
				
			|||||||
import time
 | 
					import time
 | 
				
			||||||
import asyncio
 | 
					import asyncio
 | 
				
			||||||
from app.core.database import get_last_login, get_session, set_last_login
 | 
					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):
 | 
					class MainController(QStackedWidget):
 | 
				
			||||||
    # Сигнал для показа уведомлений из любого потока
 | 
					    # Сигнал для показа уведомлений из любого потока
 | 
				
			||||||
    #      (message, is_error)
 | 
					    #      (message, is_error)
 | 
				
			||||||
    notification_requested = Signal(str, bool)
 | 
					    notification_requested = Signal(str, bool)
 | 
				
			||||||
 | 
					    # Сигнал для передачи загруженных данных чата в основной поток
 | 
				
			||||||
 | 
					    chats_loaded = Signal(object)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self):
 | 
					    def __init__(self):
 | 
				
			||||||
        super().__init__()
 | 
					        super().__init__()
 | 
				
			||||||
@ -49,8 +53,9 @@ class MainController(QStackedWidget):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        self.yobble_home_view = YobbleHomeView(username=username)
 | 
					        self.yobble_home_view = YobbleHomeView(username=username)
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # Подключаем сигнал к слоту в YobbleHomeView
 | 
					        # Подключаем сигналы к слотам в YobbleHomeView
 | 
				
			||||||
        self.notification_requested.connect(self.yobble_home_view.show_notification)
 | 
					        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.addWidget(self.yobble_home_view)
 | 
				
			||||||
        self.setCurrentWidget(self.yobble_home_view)
 | 
					        self.setCurrentWidget(self.yobble_home_view)
 | 
				
			||||||
@ -72,8 +77,32 @@ class MainController(QStackedWidget):
 | 
				
			|||||||
                # asyncio.run() выполняет async функцию и возвращает её результат
 | 
					                # asyncio.run() выполняет async функцию и возвращает её результат
 | 
				
			||||||
                asyncio.run(self.yobble_home_view.preload_permissions())
 | 
					                asyncio.run(self.yobble_home_view.preload_permissions())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # Загружаем список чатов
 | 
				
			||||||
 | 
					                print("[Sync] Загружаем список чатов...")
 | 
				
			||||||
 | 
					                asyncio.run(self.load_chats(username))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            except Exception as e:
 | 
					            except Exception as e:
 | 
				
			||||||
                error_message = f"Ошибка предзагрузки: {e}"
 | 
					                error_message = f"Ошибка предзагрузки: {e}"
 | 
				
			||||||
                print(f"[Sync] {error_message}")
 | 
					                print(f"[Sync] {error_message}")
 | 
				
			||||||
                # Отправляем сигнал вместо прямого вызова
 | 
					                # Отправляем сигнал вместо прямого вызова
 | 
				
			||||||
                self.notification_requested.emit(error_message, True)
 | 
					                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)
 | 
				
			||||||
 | 
				
			|||||||
@ -1,20 +1,47 @@
 | 
				
			|||||||
from pydantic import BaseModel, Field
 | 
					 | 
				
			||||||
from typing import Optional, Dict, Any, List, Literal
 | 
					 | 
				
			||||||
from uuid import UUID
 | 
					from uuid import UUID
 | 
				
			||||||
from datetime import datetime
 | 
					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):
 | 
					class PrivateChatListItem(BaseModel):
 | 
				
			||||||
    chat_name: Optional[str]
 | 
					    chat_id: UUID = Field(..., description="ID чата")
 | 
				
			||||||
    chat_type: Literal["self", "private"]
 | 
					    chat_type: Literal["self", "private"] = Field(..., description="Тип чата")
 | 
				
			||||||
    chat_id: UUID
 | 
					    chat_data: Optional[Dict[str, Any]] = Field(default=None, description="Данные о чате")
 | 
				
			||||||
    chat_data: Optional[Dict[str, Any]] = None
 | 
					    last_message: Optional[MessageItem] = Field(None, description="Последнее сообщение в чате")
 | 
				
			||||||
    companion_id: Optional[UUID] = None
 | 
					    created_at: datetime = Field(..., description="Дата создания чата")
 | 
				
			||||||
    companion_data: Optional[Dict[str, Any]] = None  # Типизируй как нужно
 | 
					
 | 
				
			||||||
    last_message: Optional[LastMessage] = None
 | 
					
 | 
				
			||||||
    created_at: datetime
 | 
					class PrivateChatListData(BaseModel):
 | 
				
			||||||
 | 
					    items: List[PrivateChatListItem]
 | 
				
			||||||
 | 
					    has_more: bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PrivateChatListResponse(BaseModel):
 | 
				
			||||||
 | 
					    status: str
 | 
				
			||||||
 | 
					    data: PrivateChatListData
 | 
				
			||||||
							
								
								
									
										48
									
								
								app/core/services/chat_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								app/core/services/chat_service.py
									
									
									
									
									
										Normal file
									
								
							@ -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}"
 | 
				
			||||||
@ -1,35 +1,59 @@
 | 
				
			|||||||
from PySide6.QtWidgets import QWidget, QListWidget, QVBoxLayout, QLabel, QListWidgetItem
 | 
					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
 | 
					from app.core.models.chat_models import PrivateChatListItem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ChatListView(QWidget):
 | 
					class ChatListView(QWidget):
 | 
				
			||||||
    def __init__(self, username, chat_items: list[PrivateChatListItem]):
 | 
					    def __init__(self):
 | 
				
			||||||
        super().__init__()
 | 
					        super().__init__()
 | 
				
			||||||
        self.setWindowTitle(f"Чаты — {username}")
 | 
					 | 
				
			||||||
        self.setMinimumSize(400, 500)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.chat_items = chat_items
 | 
					 | 
				
			||||||
        self.init_ui()
 | 
					        self.init_ui()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def init_ui(self):
 | 
					    def init_ui(self):
 | 
				
			||||||
        layout = QVBoxLayout()
 | 
					        """Инициализирует пользовательский интерфейс."""
 | 
				
			||||||
 | 
					        layout = QVBoxLayout(self)
 | 
				
			||||||
        self.label = QLabel("Список чатов:")
 | 
					        layout.setContentsMargins(0, 0, 0, 0)
 | 
				
			||||||
        layout.addWidget(self.label)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.chat_list = QListWidget()
 | 
					        self.chat_list = QListWidget()
 | 
				
			||||||
        self.render_chat_items()
 | 
					        self.chat_list.setStyleSheet("QListWidget { border: none; }")
 | 
				
			||||||
        layout.addWidget(self.chat_list)
 | 
					        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()
 | 
					        self.chat_list.clear()
 | 
				
			||||||
        for chat in self.chat_items:
 | 
					        item = QListWidgetItem(text)
 | 
				
			||||||
            companion_name = chat.companion_data.get("username", "Без имени") if chat.companion_data else "Неизвестный"
 | 
					        item.setTextAlignment(Qt.AlignCenter)
 | 
				
			||||||
            last_msg = chat.last_message.context if chat.last_message else "Нет сообщений"
 | 
					        self.chat_list.addItem(item)
 | 
				
			||||||
            item_text = f"{companion_name} — {last_msg}"
 | 
					 | 
				
			||||||
            self.chat_list.addItem(QListWidgetItem(item_text))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update_chat_items(self, new_items: list[PrivateChatListItem]):
 | 
					    def populate_chats(self, chat_items: List[PrivateChatListItem]):
 | 
				
			||||||
        self.chat_items = new_items
 | 
					        """
 | 
				
			||||||
        self.render_chat_items()
 | 
					        Заполняет список чатов данными, полученными от сервера.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        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)
 | 
				
			||||||
 | 
				
			|||||||
@ -11,6 +11,7 @@ from PySide6.QtGui import QColor
 | 
				
			|||||||
from app.core.theme import theme_manager
 | 
					from app.core.theme import theme_manager
 | 
				
			||||||
from app.core.dialogs import show_themed_messagebox
 | 
					from app.core.dialogs import show_themed_messagebox
 | 
				
			||||||
from app.ui.views.side_menu_view import SideMenuView
 | 
					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.services.auth_service import get_user_role
 | 
				
			||||||
from app.core.database import get_current_access_token
 | 
					from app.core.database import get_current_access_token
 | 
				
			||||||
from app.core.localizer import localizer
 | 
					from app.core.localizer import localizer
 | 
				
			||||||
@ -366,11 +367,21 @@ class YobbleHomeView(QWidget):
 | 
				
			|||||||
        self.content_stack.addWidget(self.music_label)
 | 
					        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("Контент Профиля"))
 | 
					        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):
 | 
					    def on_tab_button_clicked(self, index):
 | 
				
			||||||
        """Обрабатывает нажатие на кнопку вкладки, проверяя права доступа."""
 | 
					        """Обрабатывает нажатие на кнопку вкладки, проверяя права доступа."""
 | 
				
			||||||
        if index in self.REQUIRED_PERMISSIONS:
 | 
					        if index in self.REQUIRED_PERMISSIONS:
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user