Compare commits

..

No commits in common. "229cab0dde142783fea24525be94b6a88834329c" and "63c07305da866c1980478ca2b074f4ff374fe05a" have entirely different histories.

10 changed files with 34 additions and 461 deletions

View File

@ -45,20 +45,13 @@ 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, current_user_id=user_id)
self.yobble_home_view = YobbleHomeView(username=username)
# Подключаем сигналы к слотам в YobbleHomeView
self.notification_requested.connect(self.yobble_home_view.show_notification)

View File

@ -2,7 +2,7 @@ DEBUG = True
API_SCHEME = "https"
API_HOST = "api.yobble.org"
BASE_URL = f"{API_SCHEME}://{API_HOST}"
APP_VERSION = "0.3_chat"
APP_VERSION = "0.2_home_screen"
APP_NAME = "yobble messenger"
APP_HEADER = f"{APP_NAME}"
if DEBUG: APP_HEADER=f"{APP_HEADER} ({APP_VERSION})"

View File

@ -28,7 +28,6 @@ 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
@ -51,13 +50,11 @@ def init_db():
conn.commit()
conn.close()
def add_session(login, access_token, refresh_token, user_id=None, update_existing=False):
def add_session(login, access_token, refresh_token, 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('''
@ -68,9 +65,9 @@ def add_session(login, access_token, refresh_token, user_id=None, update_existin
else:
# Вставляем новую или заменяем существующую по логину
cursor.execute('''
INSERT OR REPLACE INTO sessions (login, user_id, access_token, refresh_token, created_at)
VALUES (?, ?, ?, ?, ?)
''', (login, user_id, access_token, refresh_token, datetime.now()))
INSERT OR REPLACE INTO sessions (login, access_token, refresh_token, created_at)
VALUES (?, ?, ?, ?)
''', (login, access_token, refresh_token, datetime.now()))
conn.commit()
conn.close()

View File

@ -45,34 +45,3 @@ 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
# 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

View File

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

View File

@ -1,12 +1,7 @@
import httpx
from app.core import config
from app.core.localizer import localizer
from app.core.models.chat_models import (
PrivateChatListResponse, PrivateChatListData,
PrivateChatHistoryResponse, PrivateChatHistoryData,
PrivateMessageSendRequest, PrivateMessageSendResponse
)
from uuid import UUID
from app.core.models.chat_models import PrivateChatListResponse, PrivateChatListData
async def get_private_chats(token: str, offset: int = 0, limit: int = 20):
"""
@ -51,91 +46,3 @@ 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}"
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}"

View File

@ -1,18 +1,14 @@
from PySide6.QtWidgets import QWidget, QListWidget, QVBoxLayout, QListWidgetItem
from PySide6.QtCore import Qt, QSize, Signal
from PySide6.QtCore import Qt, QSize
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)
@ -25,18 +21,11 @@ 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()
@ -92,7 +81,6 @@ class ChatListView(QWidget):
Заполняет список чатов данными, полученными от сервера.
"""
self.chat_list.clear()
self.chat_items_map.clear()
if not chat_items:
self.show_placeholder_message("У вас пока нет чатов")
@ -129,6 +117,3 @@ 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

View File

@ -1,124 +0,0 @@
from PySide6.QtWidgets import QWidget, QVBoxLayout, QListWidget, QLineEdit, QPushButton, QHBoxLayout, QListWidgetItem
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
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)
# --- Подключение сигналов ---
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()
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,10 +15,6 @@ 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, send_private_message
from app.core.models.chat_models import PrivateMessageSendRequest, MessageItem
from uuid import UUID
class YobbleHomeView(QWidget):
REQUIRED_PERMISSIONS = {
@ -26,17 +22,15 @@ class YobbleHomeView(QWidget):
1: "music.access" # Музыка
}
def __init__(self, username: str, current_user_id: UUID):
def __init__(self, username: str):
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, чтобы можно было разместить меню и контент рядом
@ -243,14 +237,6 @@ 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)
@ -279,6 +265,16 @@ 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):
@ -372,16 +368,11 @@ 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):
"""
Слот для обновления списка чатов.
@ -438,6 +429,19 @@ 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
@ -476,101 +480,6 @@ 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.chat_view.send_message_requested.connect(self.send_message)
# Заменяем плейсхолдер на реальный виджет
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 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)
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)
@ -706,10 +615,6 @@ class YobbleHomeView(QWidget):
color: {title_color};
background: transparent;
}}
#BackButton {{
font-size: 28px;
font-weight: bold;
}}
#TitleLabel {{
font-size: 18px;
font-weight: bold;

View File

@ -1,58 +0,0 @@
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