diff --git a/app/core/config.py b/app/core/config.py index 8b08933..c5b226f 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -2,7 +2,7 @@ DEBUG = True API_SCHEME = "https" API_HOST = "api.yobble.org" BASE_URL = f"{API_SCHEME}://{API_HOST}" -APP_VERSION = "0.2_home_screen" +APP_VERSION = "0.3_chat" APP_NAME = "yobble messenger" APP_HEADER = f"{APP_NAME}" if DEBUG: APP_HEADER=f"{APP_HEADER} ({APP_VERSION})" diff --git a/app/core/models/chat_models.py b/app/core/models/chat_models.py index 02e7cfd..d600d8d 100644 --- a/app/core/models/chat_models.py +++ b/app/core/models/chat_models.py @@ -55,4 +55,24 @@ class PrivateChatHistoryData(BaseModel): class PrivateChatHistoryResponse(BaseModel): status: str - data: PrivateChatHistoryData \ No newline at end of file + data: PrivateChatHistoryData + + +# send +class PrivateMessageSendRequest(BaseModel): + chat_id: UUID + content: Optional[str] = Field(None, description="Содержимое сообщения (макс. 4096 символов)", max_length=4096) + message_type: List[Literal["text"]] = Field( + ..., description="Один или несколько типов сообщения" + ) + + +class PrivateMessageSendData(BaseModel): + message_id: int + chat_id: UUID + created_at: datetime + + +class PrivateMessageSendResponse(BaseModel): + status: str + data: PrivateMessageSendData \ No newline at end of file diff --git a/app/core/services/chat_service.py b/app/core/services/chat_service.py index 750c23b..913d3b4 100644 --- a/app/core/services/chat_service.py +++ b/app/core/services/chat_service.py @@ -1,7 +1,11 @@ import httpx from app.core import config from app.core.localizer import localizer -from app.core.models.chat_models import PrivateChatListResponse, PrivateChatListData, PrivateChatHistoryResponse, PrivateChatHistoryData +from app.core.models.chat_models import ( + PrivateChatListResponse, PrivateChatListData, + PrivateChatHistoryResponse, PrivateChatHistoryData, + PrivateMessageSendRequest, PrivateMessageSendResponse +) from uuid import UUID async def get_private_chats(token: str, offset: int = 0, limit: int = 20): @@ -90,3 +94,48 @@ async def get_chat_history(token: str, chat_id: UUID, before_message_id: int = N return False, f"Ошибка сети: {e}" except Exception as e: return False, f"Произошла ошибка: {e}" + + +async def send_private_message(token: str, payload: "PrivateMessageSendRequest"): + """ + Отправляет приватное сообщение в чат. + + :param token: Токен доступа + :param payload: Данные сообщения (Pydantic модель PrivateMessageSendRequest) + :return: Кортеж (успех: bool, данные: PrivateMessageSendData | str) + """ + url = f"{config.BASE_URL}/v1/chat/private/send" + headers = {"Authorization": f"Bearer {token}"} + + try: + async with httpx.AsyncClient(http2=True) as client: + response = await client.post(url, headers=headers, json=payload.model_dump(mode='json')) + + if response.status_code == 200: + data = response.json() + if data.get("status") == "fine": + response_model = PrivateMessageSendResponse(**data) + return True, response_model.data + else: + return False, data.get("detail", "Неизвестная ошибка ответа") + + elif response.status_code in [401, 403, 404]: + error_data = response.json() + print("error_data", error_data) + return False, error_data.get("detail", "Ошибка доступа или чат не найден") + + elif response.status_code == 422: + error_data = response.json() + # Может быть список ошибок + detail = error_data.get("detail") + if isinstance(detail, list): + return False, ", ".join([e.get("msg", "Неизвестная ошибка валидации") for e in detail]) + return False, detail or "Некорректные данные" + + else: + return False, f"Ошибка сервера: {response.status_code}" + + except httpx.RequestError as e: + return False, f"Ошибка сети: {e}" + except Exception as e: + return False, f"Произошла ошибка: {e}" diff --git a/app/ui/views/chat_view.py b/app/ui/views/chat_view.py index 2808c39..95f2749 100644 --- a/app/ui/views/chat_view.py +++ b/app/ui/views/chat_view.py @@ -1,11 +1,14 @@ from PySide6.QtWidgets import QWidget, QVBoxLayout, QListWidget, QLineEdit, QPushButton, QHBoxLayout, QListWidgetItem -from PySide6.QtCore import Qt +from PySide6.QtCore import Qt, Signal from app.core.models.chat_models import MessageItem from app.ui.widgets.message_bubble_widget import MessageBubbleWidget from app.core.theme import theme_manager from uuid import UUID class ChatView(QWidget): + # Сигнал, который отправляет текст сообщения для отправки + send_message_requested = Signal(str) + def __init__(self, chat_id: UUID, current_user_id: UUID): super().__init__() self.chat_id = chat_id @@ -39,6 +42,20 @@ class ChatView(QWidget): main_layout.addWidget(self.message_list) main_layout.addLayout(input_layout) + # --- Подключение сигналов --- + self.send_button.clicked.connect(self._on_send) + self.message_input.returnPressed.connect(self._on_send) + + def _on_send(self): + """Обработчик нажатия кнопки отправки.""" + message_text = self.message_input.text().strip() + if message_text: + self.send_message_requested.emit(message_text) + + def clear_input(self): + """Очищает поле ввода.""" + self.message_input.clear() + def update_theme(self): """Обновляет стили в соответствии с темой.""" palette = theme_manager.get_current_palette() diff --git a/app/ui/views/yobble_home_view.py b/app/ui/views/yobble_home_view.py index a5ded72..aa37098 100644 --- a/app/ui/views/yobble_home_view.py +++ b/app/ui/views/yobble_home_view.py @@ -16,7 +16,8 @@ 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 from app.ui.views.chat_view import ChatView -from app.core.services.chat_service import get_chat_history +from app.core.services.chat_service import get_chat_history, send_private_message +from app.core.models.chat_models import PrivateMessageSendRequest, MessageItem from uuid import UUID class YobbleHomeView(QWidget): @@ -497,6 +498,8 @@ class YobbleHomeView(QWidget): self.chat_view = ChatView(chat_id, self.current_user_id) self.chat_view.populate_history(data.items) + # Подключаем сигнал отправки сообщения к нашему новому методу + self.chat_view.send_message_requested.connect(self.send_message) # Заменяем плейсхолдер на реальный виджет self.content_stack.removeWidget(self.chat_view_container) @@ -509,6 +512,51 @@ class YobbleHomeView(QWidget): else: self.show_notification(f"Не удалось загрузить историю: {data}", is_error=True) + def send_message(self, content: str): + """Слот для отправки сообщения. Запускает асинхронную задачу.""" + if not self.chat_view: + return + + chat_id = self.chat_view.chat_id + payload = PrivateMessageSendRequest( + chat_id=chat_id, + content=content, + message_type=["text"] + ) + asyncio.create_task(self.do_send_message(payload)) + + async def do_send_message(self, payload: PrivateMessageSendRequest): + """Асинхронно отправляет сообщение и обновляет UI.""" + token = await get_current_access_token() + if not token: + self.show_notification("Ошибка: сессия не найдена.", is_error=True) + return + + success, data = await send_private_message(token, payload) + + if success: + # В случае успеха, создаем объект сообщения и добавляем в чат + # Используем текущее время, т.к. сервер возвращает время с UTC + from datetime import datetime + + new_message = MessageItem( + message_id=data.message_id, + chat_id=data.chat_id, + sender_id=self.current_user_id, + content=payload.content, + message_type=['text'], + is_viewed=True, # Свои сообщения считаем просмотренными + created_at=datetime.now(), + forward_metadata=None, + sender_data=None, + media_link=None, + updated_at=None + ) + self.chat_view.add_message(new_message) + self.chat_view.clear_input() # Очищаем поле ввода + else: + self.show_notification(f"Ошибка отправки: {data}", is_error=True) + def close_chat_view(self): """Закрывает виджет чата и возвращается к списку.""" self.content_stack.setCurrentWidget(self.chat_list_view)