add chat history

This commit is contained in:
unknown 2025-09-29 02:43:25 +03:00
parent 63c07305da
commit 96fe4d0b91
9 changed files with 326 additions and 33 deletions

View File

@ -45,13 +45,20 @@ class MainController(QStackedWidget):
def handle_login_success(self, username: str):
"""Обрабатывает успешный вход в систему."""
set_last_login(username)
session = get_session(username)
user_id = session["user_id"] if session else None
if not user_id:
self.show_login()
self.notification_requested.emit("Не удалось получить ID пользователя из сессии.", True)
return
if self.login_view:
self.login_view.close()
self.removeWidget(self.login_view)
self.login_view = None
self.yobble_home_view = YobbleHomeView(username=username)
self.yobble_home_view = YobbleHomeView(username=username, current_user_id=user_id)
# Подключаем сигналы к слотам в YobbleHomeView
self.notification_requested.connect(self.yobble_home_view.show_notification)

View File

@ -28,6 +28,7 @@ def init_db():
cursor.execute('''
CREATE TABLE IF NOT EXISTS sessions (
login TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
access_token TEXT NOT NULL,
refresh_token TEXT NOT NULL,
created_at TIMESTAMP NOT NULL
@ -50,11 +51,13 @@ def init_db():
conn.commit()
conn.close()
def add_session(login, access_token, refresh_token, update_existing=False):
def add_session(login, access_token, refresh_token, user_id=None, update_existing=False):
"""Добавляет новую сессию или обновляет существующую."""
conn = get_connection()
cursor = conn.cursor()
print("ffff", login, access_token, refresh_token, user_id, update_existing)
if update_existing:
# Обновляем существующую сессию по access_token
cursor.execute('''
@ -65,9 +68,9 @@ def add_session(login, access_token, refresh_token, update_existing=False):
else:
# Вставляем новую или заменяем существующую по логину
cursor.execute('''
INSERT OR REPLACE INTO sessions (login, access_token, refresh_token, created_at)
VALUES (?, ?, ?, ?)
''', (login, access_token, refresh_token, datetime.now()))
INSERT OR REPLACE INTO sessions (login, user_id, access_token, refresh_token, created_at)
VALUES (?, ?, ?, ?, ?)
''', (login, user_id, access_token, refresh_token, datetime.now()))
conn.commit()
conn.close()

View File

@ -45,3 +45,14 @@ class PrivateChatListData(BaseModel):
class PrivateChatListResponse(BaseModel):
status: str
data: PrivateChatListData
# history
class PrivateChatHistoryData(BaseModel):
items: List[MessageItem]
has_more: bool
class PrivateChatHistoryResponse(BaseModel):
status: str
data: PrivateChatHistoryData

View File

@ -25,7 +25,8 @@ async def login(login, password):
add_session(
login=login,
access_token=token_data["access_token"],
refresh_token=token_data["refresh_token"]
refresh_token=token_data["refresh_token"],
user_id=token_data["user_id"]
)
return True, localizer.translate("Успешный вход")
else:

View File

@ -1,7 +1,8 @@
import httpx
from app.core import config
from app.core.localizer import localizer
from app.core.models.chat_models import PrivateChatListResponse, PrivateChatListData
from app.core.models.chat_models import PrivateChatListResponse, PrivateChatListData, PrivateChatHistoryResponse, PrivateChatHistoryData
from uuid import UUID
async def get_private_chats(token: str, offset: int = 0, limit: int = 20):
"""
@ -46,3 +47,46 @@ async def get_private_chats(token: str, offset: int = 0, limit: int = 20):
return False, f"{localizer.translate('Ошибка сети')}: {e}"
except Exception as e:
return False, f"{localizer.translate('Произошла ошибка')}: {e}"
async def get_chat_history(token: str, chat_id: UUID, before_message_id: int = None, limit: int = 30):
"""
Получает историю сообщений для указанного приватного чата.
:param token: Токен доступа
:param chat_id: ID чата
:param before_message_id: ID сообщения для пагинации (загрузка более старых)
:param limit: Количество сообщений для загрузки
:return: Кортеж (успех: bool, данные: PrivateChatHistoryData | str)
"""
url = f"{config.BASE_URL}/v1/chat/private/history"
headers = {"Authorization": f"Bearer {token}"}
params = {"chat_id": str(chat_id), "limit": limit}
if before_message_id:
params["before_message_id"] = before_message_id
try:
async with httpx.AsyncClient(http2=True) as client:
response = await client.get(url, headers=headers, params=params)
if response.status_code == 200:
data = response.json()
if data.get("status") == "fine":
response_model = PrivateChatHistoryResponse(**data)
return True, response_model.data
else:
return False, data.get("detail", "Неизвестная ошибка ответа")
elif response.status_code in [401, 403]:
error_data = response.json()
return False, error_data.get("detail", "Ошибка аутентификации или доступа")
elif response.status_code == 422:
return False, "Некорректные параметры запроса"
else:
return False, f"Ошибка сервера: {response.status_code}"
except httpx.RequestError as e:
return False, f"Ошибка сети: {e}"
except Exception as e:
return False, f"Произошла ошибка: {e}"

View File

@ -1,14 +1,18 @@
from PySide6.QtWidgets import QWidget, QListWidget, QVBoxLayout, QListWidgetItem
from PySide6.QtCore import Qt, QSize
from PySide6.QtCore import Qt, QSize, Signal
from typing import List
from app.core.models.chat_models import PrivateChatListItem
from app.ui.widgets.chat_list_item_widget import ChatListItemWidget
from app.core.theme import theme_manager
from datetime import datetime
from uuid import UUID
class ChatListView(QWidget):
chat_selected = Signal(UUID)
def __init__(self):
super().__init__()
self.chat_items_map = {}
self.init_ui()
self.update_theme()
theme_manager.theme_changed.connect(self.update_theme)
@ -21,11 +25,18 @@ class ChatListView(QWidget):
self.chat_list = QListWidget()
self.chat_list.setSpacing(2)
self.chat_list.itemClicked.connect(self.on_chat_item_clicked)
layout.addWidget(self.chat_list)
# Изначальное состояние
self.show_placeholder_message("Загрузка чатов...")
def on_chat_item_clicked(self, item: QListWidgetItem):
"""Обработчик клика по элементу списка чатов."""
chat_id = self.chat_items_map.get(id(item))
if chat_id:
self.chat_selected.emit(chat_id)
def update_theme(self):
"""Обновляет стили в соответствии с темой."""
palette = theme_manager.get_current_palette()
@ -81,6 +92,7 @@ class ChatListView(QWidget):
Заполняет список чатов данными, полученными от сервера.
"""
self.chat_list.clear()
self.chat_items_map.clear()
if not chat_items:
self.show_placeholder_message("У вас пока нет чатов")
@ -117,3 +129,6 @@ class ChatListView(QWidget):
list_item.setSizeHint(item_widget.sizeHint())
self.chat_list.addItem(list_item)
self.chat_list.setItemWidget(list_item, item_widget)
# Сохраняем ID чата в словаре
self.chat_items_map[id(list_item)] = chat.chat_id

107
app/ui/views/chat_view.py Normal file
View File

@ -0,0 +1,107 @@
from PySide6.QtWidgets import QWidget, QVBoxLayout, QListWidget, QLineEdit, QPushButton, QHBoxLayout, QListWidgetItem
from PySide6.QtCore import Qt
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):
def __init__(self, chat_id: UUID, current_user_id: UUID):
super().__init__()
self.chat_id = chat_id
self.current_user_id = current_user_id
self.init_ui()
self.update_theme()
theme_manager.theme_changed.connect(self.update_theme)
def init_ui(self):
"""Инициализирует пользовательский интерфейс."""
main_layout = QVBoxLayout(self)
main_layout.setSpacing(0)
main_layout.setContentsMargins(0, 0, 0, 0)
self.message_list = QListWidget()
self.message_list.setSpacing(10)
self.message_list.setWordWrap(True)
input_layout = QHBoxLayout()
input_layout.setSpacing(10)
input_layout.setContentsMargins(10, 10, 10, 10)
self.message_input = QLineEdit()
self.message_input.setPlaceholderText("Введите сообщение...")
self.send_button = QPushButton("Отправить")
input_layout.addWidget(self.message_input)
input_layout.addWidget(self.send_button)
main_layout.addWidget(self.message_list)
main_layout.addLayout(input_layout)
def update_theme(self):
"""Обновляет стили в соответствии с темой."""
palette = theme_manager.get_current_palette()
self.setStyleSheet(f"background-color: {palette['primary']};")
self.message_list.setStyleSheet(f"""
QListWidget {{
background-color: {palette['primary']};
border: none;
padding: 10px;
}}
""")
self.message_input.setStyleSheet(f"""
QLineEdit {{
background-color: {palette['secondary']};
color: {palette['text']};
border: 1px solid {palette['border']};
border-radius: 15px;
padding: 5px 15px;
font-size: 10pt;
}}
""")
self.send_button.setStyleSheet(f"""
QPushButton {{
background-color: {palette['accent']};
color: #ffffff;
border: none;
border-radius: 15px;
padding: 5px 15px;
font-size: 10pt;
font-weight: bold;
}}
QPushButton:hover {{
background-color: #4a8ac0;
}}
""")
def add_message(self, message: MessageItem):
"""Добавляет сообщение в список."""
is_own = message.sender_id == self.current_user_id
bubble = MessageBubbleWidget(
text=message.content,
timestamp=message.created_at.strftime('%H:%M'),
is_own=is_own
)
item = QListWidgetItem(self.message_list)
item.setSizeHint(bubble.sizeHint())
# Выравнивание сообщения
if is_own:
item.setTextAlignment(Qt.AlignRight)
else:
item.setTextAlignment(Qt.AlignLeft)
self.message_list.addItem(item)
self.message_list.setItemWidget(item, bubble)
self.message_list.scrollToBottom()
def populate_history(self, messages: list[MessageItem]):
"""Заполняет список сообщениями из истории."""
self.message_list.clear()
# Сортируем сообщения по дате (от старых к новым)
messages.sort(key=lambda m: m.created_at)
for message in messages:
self.add_message(message)

View File

@ -15,6 +15,9 @@ 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
from app.ui.views.chat_view import ChatView
from app.core.services.chat_service import get_chat_history
from uuid import UUID
class YobbleHomeView(QWidget):
REQUIRED_PERMISSIONS = {
@ -22,15 +25,17 @@ class YobbleHomeView(QWidget):
1: "music.access" # Музыка
}
def __init__(self, username: str):
def __init__(self, username: str, current_user_id: UUID):
super().__init__()
self.username = username
self.current_user_id = current_user_id
self.setWindowTitle(f"Yobble Home - {username}")
self.setMinimumSize(360, 640)
self.permission_cache = set()
self.permissions_preloaded = False
self.permissions_preloaded = False
self.permissions_preloaded_last = 0.0
self.chat_view = None # Placeholder for the chat view
# --- Основной макет ---
# Используем QHBoxLayout, чтобы можно было разместить меню и контент рядом
@ -237,6 +242,14 @@ class YobbleHomeView(QWidget):
top_bar_layout = QHBoxLayout(top_bar_widget)
top_bar_layout.setContentsMargins(10, 5, 10, 5)
self.back_button = QPushButton("")
self.back_button.setObjectName("BackButton")
self.back_button.setFocusPolicy(Qt.NoFocus)
self.back_button.setCursor(Qt.PointingHandCursor)
self.back_button.clicked.connect(self.close_chat_view)
self.back_button.hide()
top_bar_layout.addWidget(self.back_button)
self.burger_menu_button = QPushButton("")
self.burger_menu_button.setObjectName("BurgerMenuButton")
self.burger_menu_button.setFocusPolicy(Qt.NoFocus)
@ -265,16 +278,6 @@ class YobbleHomeView(QWidget):
top_bar_layout.addWidget(self.notification_button)
self.notification_button.clicked.connect(self.handle_notification_click)
# --- Временные кнопки для теста ---
# self.test_ok_button = QPushButton("Test OK")
# self.test_ok_button.clicked.connect(lambda: self.show_notification("Операция прошла успешно", is_error=False))
# top_bar_layout.addWidget(self.test_ok_button)
# self.test_err_button = QPushButton("Test Error")
# self.test_err_button.clicked.connect(lambda: self.show_notification("Произошла ошибка при обновлении", is_error=True))
# top_bar_layout.addWidget(self.test_err_button)
# --- Конец временного кода ---
return top_bar_widget
def create_bottom_bar(self):
@ -368,11 +371,16 @@ class YobbleHomeView(QWidget):
# Чаты
self.chat_list_view = ChatListView()
self.chat_list_view.chat_selected.connect(self.open_chat_view)
self.content_stack.addWidget(self.chat_list_view)
# Профиль
self.content_stack.addWidget(QLabel("Контент Профиля"))
# Placeholder для открытого чата
self.chat_view_container = QWidget()
self.content_stack.addWidget(self.chat_view_container)
def update_chat_list(self, chat_data):
"""
Слот для обновления списка чатов.
@ -429,19 +437,6 @@ class YobbleHomeView(QWidget):
raise ConnectionError(data)
return success, data
# def update_current_tab_content(self):
# """Обновляет контент текущей активной вкладки после предзагрузки прав."""
# current_index = self.content_stack.currentIndex()
# # Проверяем, требует ли текущая вкладка прав доступа
# if current_index in self.REQUIRED_PERMISSIONS:
# permission_code = self.REQUIRED_PERMISSIONS[current_index]
# if permission_code in self.permission_cache:
# self.show_real_content(current_index)
# else:
# self.show_denied(current_index)
async def check_permissions_and_switch(self, index, permission_code):
"""Асинхронно проверяет права и переключает вкладку."""
self.preload_permissions_first = False
@ -480,6 +475,54 @@ class YobbleHomeView(QWidget):
titles = ["Лента", "Музыка", "Чаты", "Лицо"]
self.title_label.setText(titles[index])
def open_chat_view(self, chat_id: UUID):
"""Открывает виджет чата, загружая его историю."""
print(f"Opening chat: {chat_id}")
asyncio.create_task(self.load_and_display_chat(chat_id))
async def load_and_display_chat(self, chat_id: UUID):
"""Асинхронно загружает историю и отображает чат."""
token = await get_current_access_token()
if not token:
self.show_notification("Ошибка: сессия не найдена.", is_error=True)
return
self.show_notification("Загрузка истории...", is_error=False, duration=1000)
success, data = await get_chat_history(token, chat_id)
if success:
if self.chat_view:
self.content_stack.removeWidget(self.chat_view)
self.chat_view.deleteLater()
self.chat_view = ChatView(chat_id, self.current_user_id)
self.chat_view.populate_history(data.items)
# Заменяем плейсхолдер на реальный виджет
self.content_stack.removeWidget(self.chat_view_container)
self.content_stack.addWidget(self.chat_view)
self.content_stack.setCurrentWidget(self.chat_view)
self.bottom_bar.hide()
self.burger_menu_button.hide()
self.back_button.show()
else:
self.show_notification(f"Не удалось загрузить историю: {data}", is_error=True)
def close_chat_view(self):
"""Закрывает виджет чата и возвращается к списку."""
self.content_stack.setCurrentWidget(self.chat_list_view)
self.bottom_bar.show()
self.burger_menu_button.show()
self.back_button.hide()
if self.chat_view:
self.content_stack.removeWidget(self.chat_view)
self.chat_view.deleteLater()
self.chat_view = None
# Возвращаем плейсхолдер
self.chat_view_container = QWidget()
self.content_stack.addWidget(self.chat_view_container)
def show_error_message(self, message):
"""Показывает всплывающее уведомление об ошибке."""
self.show_notification(message, is_error=True)
@ -615,6 +658,10 @@ class YobbleHomeView(QWidget):
color: {title_color};
background: transparent;
}}
#BackButton {{
font-size: 28px;
font-weight: bold;
}}
#TitleLabel {{
font-size: 18px;
font-weight: bold;

View File

@ -0,0 +1,58 @@
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel
from PySide6.QtCore import Qt
from app.core.theme import theme_manager
class MessageBubbleWidget(QWidget):
def __init__(self, text: str, timestamp: str, is_own: bool):
super().__init__()
self.init_ui(text, timestamp, is_own)
self.update_theme()
def init_ui(self, text: str, timestamp: str, is_own: bool):
"""Инициализирует пользовательский интерфейс."""
self.layout = QVBoxLayout(self)
self.layout.setSpacing(2)
self.text_label = QLabel(text)
self.text_label.setWordWrap(True)
self.timestamp_label = QLabel(timestamp)
self.timestamp_label.setAlignment(Qt.AlignRight)
self.layout.addWidget(self.text_label)
self.layout.addWidget(self.timestamp_label)
self.is_own = is_own
self.setObjectName("MessageBubble")
self.update_theme()
def update_theme(self):
"""Обновляет стили виджета в соответствии с текущей темой."""
palette = theme_manager.get_current_palette()
if self.is_own:
bg_color = palette['accent']
text_color = "#ffffff"
timestamp_color = "#dddddd"
alignment = Qt.AlignRight
else:
bg_color = palette['secondary']
text_color = palette['text']
timestamp_color = palette['text_secondary']
alignment = Qt.AlignLeft
self.setStyleSheet(f"""
#MessageBubble {{
background-color: {bg_color};
border-radius: 10px;
padding: 8px 12px;
}}
""")
self.text_label.setStyleSheet(f"color: {text_color}; font-size: 10pt;")
self.timestamp_label.setStyleSheet(f"color: {timestamp_color}; font-size: 8pt;")
# Устанавливаем выравнивание для всего layout
parent_layout = self.parentWidget().layout() if self.parentWidget() else None
if parent_layout:
# Этот способ не сработает напрямую, выравнивание нужно делать в QListWidgetItem
pass