add chat history
This commit is contained in:
parent
63c07305da
commit
96fe4d0b91
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
@ -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:
|
||||
|
||||
@ -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}"
|
||||
|
||||
@ -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
107
app/ui/views/chat_view.py
Normal 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)
|
||||
@ -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;
|
||||
|
||||
58
app/ui/widgets/message_bubble_widget.py
Normal file
58
app/ui/widgets/message_bubble_widget.py
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user