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): def handle_login_success(self, username: str):
"""Обрабатывает успешный вход в систему.""" """Обрабатывает успешный вход в систему."""
set_last_login(username) 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: if self.login_view:
self.login_view.close() self.login_view.close()
self.removeWidget(self.login_view) self.removeWidget(self.login_view)
self.login_view = None self.login_view = None
self.yobble_home_view = YobbleHomeView(username=username) self.yobble_home_view = YobbleHomeView(username=username, current_user_id=user_id)
# Подключаем сигналы к слотам в YobbleHomeView # Подключаем сигналы к слотам в YobbleHomeView
self.notification_requested.connect(self.yobble_home_view.show_notification) self.notification_requested.connect(self.yobble_home_view.show_notification)

View File

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

View File

@ -44,4 +44,15 @@ class PrivateChatListData(BaseModel):
class PrivateChatListResponse(BaseModel): class PrivateChatListResponse(BaseModel):
status: str status: str
data: PrivateChatListData 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( add_session(
login=login, login=login,
access_token=token_data["access_token"], 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("Успешный вход") return True, localizer.translate("Успешный вход")
else: else:

View File

@ -1,7 +1,8 @@
import httpx import httpx
from app.core import config from app.core import config
from app.core.localizer import localizer 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): 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}" return False, f"{localizer.translate('Ошибка сети')}: {e}"
except Exception as e: except Exception as e:
return False, f"{localizer.translate('Произошла ошибка')}: {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.QtWidgets import QWidget, QListWidget, QVBoxLayout, QListWidgetItem
from PySide6.QtCore import Qt, QSize from PySide6.QtCore import Qt, QSize, Signal
from typing import List from typing import List
from app.core.models.chat_models import PrivateChatListItem from app.core.models.chat_models import PrivateChatListItem
from app.ui.widgets.chat_list_item_widget import ChatListItemWidget from app.ui.widgets.chat_list_item_widget import ChatListItemWidget
from app.core.theme import theme_manager from app.core.theme import theme_manager
from datetime import datetime from datetime import datetime
from uuid import UUID
class ChatListView(QWidget): class ChatListView(QWidget):
chat_selected = Signal(UUID)
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.chat_items_map = {}
self.init_ui() self.init_ui()
self.update_theme() self.update_theme()
theme_manager.theme_changed.connect(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 = QListWidget()
self.chat_list.setSpacing(2) self.chat_list.setSpacing(2)
self.chat_list.itemClicked.connect(self.on_chat_item_clicked)
layout.addWidget(self.chat_list) layout.addWidget(self.chat_list)
# Изначальное состояние # Изначальное состояние
self.show_placeholder_message("Загрузка чатов...") 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): def update_theme(self):
"""Обновляет стили в соответствии с темой.""" """Обновляет стили в соответствии с темой."""
palette = theme_manager.get_current_palette() palette = theme_manager.get_current_palette()
@ -81,6 +92,7 @@ class ChatListView(QWidget):
Заполняет список чатов данными, полученными от сервера. Заполняет список чатов данными, полученными от сервера.
""" """
self.chat_list.clear() self.chat_list.clear()
self.chat_items_map.clear()
if not chat_items: if not chat_items:
self.show_placeholder_message("У вас пока нет чатов") self.show_placeholder_message("У вас пока нет чатов")
@ -117,3 +129,6 @@ class ChatListView(QWidget):
list_item.setSizeHint(item_widget.sizeHint()) list_item.setSizeHint(item_widget.sizeHint())
self.chat_list.addItem(list_item) self.chat_list.addItem(list_item)
self.chat_list.setItemWidget(list_item, item_widget) 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.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
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): class YobbleHomeView(QWidget):
REQUIRED_PERMISSIONS = { REQUIRED_PERMISSIONS = {
@ -22,15 +25,17 @@ class YobbleHomeView(QWidget):
1: "music.access" # Музыка 1: "music.access" # Музыка
} }
def __init__(self, username: str): def __init__(self, username: str, current_user_id: UUID):
super().__init__() super().__init__()
self.username = username self.username = username
self.current_user_id = current_user_id
self.setWindowTitle(f"Yobble Home - {username}") self.setWindowTitle(f"Yobble Home - {username}")
self.setMinimumSize(360, 640) self.setMinimumSize(360, 640)
self.permission_cache = set() self.permission_cache = set()
self.permissions_preloaded = False self.permissions_preloaded = False
self.permissions_preloaded = False self.permissions_preloaded = False
self.permissions_preloaded_last = 0.0 self.permissions_preloaded_last = 0.0
self.chat_view = None # Placeholder for the chat view
# --- Основной макет --- # --- Основной макет ---
# Используем QHBoxLayout, чтобы можно было разместить меню и контент рядом # Используем QHBoxLayout, чтобы можно было разместить меню и контент рядом
@ -237,6 +242,14 @@ class YobbleHomeView(QWidget):
top_bar_layout = QHBoxLayout(top_bar_widget) top_bar_layout = QHBoxLayout(top_bar_widget)
top_bar_layout.setContentsMargins(10, 5, 10, 5) 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 = QPushButton("")
self.burger_menu_button.setObjectName("BurgerMenuButton") self.burger_menu_button.setObjectName("BurgerMenuButton")
self.burger_menu_button.setFocusPolicy(Qt.NoFocus) self.burger_menu_button.setFocusPolicy(Qt.NoFocus)
@ -265,16 +278,6 @@ class YobbleHomeView(QWidget):
top_bar_layout.addWidget(self.notification_button) top_bar_layout.addWidget(self.notification_button)
self.notification_button.clicked.connect(self.handle_notification_click) 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 return top_bar_widget
def create_bottom_bar(self): def create_bottom_bar(self):
@ -368,11 +371,16 @@ class YobbleHomeView(QWidget):
# Чаты # Чаты
self.chat_list_view = ChatListView() 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(self.chat_list_view)
# Профиль # Профиль
self.content_stack.addWidget(QLabel("Контент Профиля")) 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): def update_chat_list(self, chat_data):
""" """
Слот для обновления списка чатов. Слот для обновления списка чатов.
@ -429,19 +437,6 @@ class YobbleHomeView(QWidget):
raise ConnectionError(data) raise ConnectionError(data)
return success, 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): async def check_permissions_and_switch(self, index, permission_code):
"""Асинхронно проверяет права и переключает вкладку.""" """Асинхронно проверяет права и переключает вкладку."""
self.preload_permissions_first = False self.preload_permissions_first = False
@ -480,6 +475,54 @@ class YobbleHomeView(QWidget):
titles = ["Лента", "Музыка", "Чаты", "Лицо"] titles = ["Лента", "Музыка", "Чаты", "Лицо"]
self.title_label.setText(titles[index]) 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): def show_error_message(self, message):
"""Показывает всплывающее уведомление об ошибке.""" """Показывает всплывающее уведомление об ошибке."""
self.show_notification(message, is_error=True) self.show_notification(message, is_error=True)
@ -615,6 +658,10 @@ class YobbleHomeView(QWidget):
color: {title_color}; color: {title_color};
background: transparent; background: transparent;
}} }}
#BackButton {{
font-size: 28px;
font-weight: bold;
}}
#TitleLabel {{ #TitleLabel {{
font-size: 18px; font-size: 18px;
font-weight: bold; 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