diff --git a/app/ui/views/chat_list_view.py b/app/ui/views/chat_list_view.py index 54e8c75..b704b5b 100644 --- a/app/ui/views/chat_list_view.py +++ b/app/ui/views/chat_list_view.py @@ -106,7 +106,10 @@ class ChatListView(QWidget): if chat.chat_type == "self": companion_name = "Избранное" elif chat.chat_data and 'login' in chat.chat_data: - companion_name = chat.chat_data['login'] + if chat.chat_data['full_name']: + companion_name = chat.chat_data['full_name'] + else: + companion_name = chat.chat_data['login'] else: companion_name = "Неизвестный" diff --git a/app/ui/views/chat_view.py b/app/ui/views/chat_view.py index 95f2749..15eecea 100644 --- a/app/ui/views/chat_view.py +++ b/app/ui/views/chat_view.py @@ -5,8 +5,9 @@ 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): @@ -18,7 +19,7 @@ class ChatView(QWidget): 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) @@ -32,8 +33,8 @@ class ChatView(QWidget): input_layout.setContentsMargins(10, 10, 10, 10) self.message_input = QLineEdit() - self.message_input.setPlaceholderText("Введите сообщение...") - + self.message_input.setPlaceholderText("Напишите сообщение…") + self.send_button = QPushButton("Отправить") input_layout.addWidget(self.message_input) @@ -42,22 +43,22 @@ class ChatView(QWidget): main_layout.addWidget(self.message_list) main_layout.addLayout(input_layout) - # --- Подключение сигналов --- + # wire events 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""" @@ -93,32 +94,51 @@ class ChatView(QWidget): """) def add_message(self, message: MessageItem): - """Добавляет сообщение в список.""" + """Добавить одно сообщение в список.""" is_own = message.sender_id == self.current_user_id + print("debug message", message) + # Имя отправителя для входящих (best-effort) + sender_name = None + if not is_own and getattr(message, 'sender_data', None): + sd = message.sender_data + try: + if isinstance(sd, dict): + sender_name = sd.get('display_name') or sd.get('full_name') or sd.get('name') or sd.get('username') + else: + sender_name = ( + getattr(sd, 'display_name', None) + or getattr(sd, 'full_name', None) + or getattr(sd, 'name', None) + or getattr(sd, 'username', None) + ) + except Exception: + sender_name = None + bubble = MessageBubbleWidget( text=message.content, timestamp=message.created_at.strftime('%H:%M'), - is_own=is_own + is_own=is_own, + sender_name=sender_name ) - + 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) + diff --git a/app/ui/widgets/message_bubble_widget.py b/app/ui/widgets/message_bubble_widget.py index c22e1d3..ea4bd40 100644 --- a/app/ui/widgets/message_bubble_widget.py +++ b/app/ui/widgets/message_bubble_widget.py @@ -1,58 +1,131 @@ -from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel +from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout +from PySide6.QtGui import QPixmap, QPainter, QColor, QBrush 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) +class MessageBubbleWidget(QWidget): + def __init__( + self, + text: str, + timestamp: str, + is_own: bool, + sender_name: str | None = None, + avatar_path: str | None = None, + ): + super().__init__() + self.is_own = is_own + self.sender_name = sender_name + self.avatar_path = avatar_path + self._build_ui(text, timestamp) + self.update_theme() + # React to theme changes + try: + theme_manager.theme_changed.connect(lambda *_: self.update_theme()) + except Exception: + pass + + def _build_ui(self, text: str, timestamp: str): + # Main row controls left/right alignment + main_layout = QHBoxLayout(self) + main_layout.setContentsMargins(6, 2, 6, 2) + main_layout.setSpacing(8) + + # Avatar placeholder (visible for incoming) + self.avatar_label = QLabel() + self._set_avatar(self.avatar_path) + + # Stack with optional sender name and the bubble + stack_layout = QVBoxLayout() + stack_layout.setSpacing(2) + stack_layout.setContentsMargins(0, 0, 0, 0) + + # Sender name only for incoming messages (if provided) + self.name_label = QLabel(self.sender_name or "") + self.name_label.setVisible(bool(self.sender_name) and not self.is_own) + + # Bubble frame holds text and timestamp + self.bubble_frame = QWidget() + self.bubble_frame.setObjectName("Bubble") + bubble_layout = QVBoxLayout(self.bubble_frame) + bubble_layout.setContentsMargins(12, 8, 12, 6) + bubble_layout.setSpacing(3) 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) + bubble_layout.addWidget(self.text_label) + bubble_layout.addWidget(self.timestamp_label, 0, Qt.AlignRight if self.is_own else Qt.AlignLeft) - self.is_own = is_own - self.setObjectName("MessageBubble") - self.update_theme() + stack_layout.addWidget(self.name_label) + stack_layout.addWidget(self.bubble_frame) + + # Arrange horizontally based on ownership: right for own, left for others + if self.is_own: + main_layout.addStretch(1) + main_layout.addLayout(stack_layout) + # Reserve place for avatar on the right, hidden for own + self.avatar_label.setVisible(False) + main_layout.addWidget(self.avatar_label) + else: + main_layout.addWidget(self.avatar_label) + main_layout.addLayout(stack_layout) + main_layout.addStretch(1) + + # Limit bubble width for readability + self.bubble_frame.setMaximumWidth(420) + + def _set_avatar(self, image_path: str | None): + size = 32 + if image_path: + pixmap = QPixmap(image_path) + if pixmap.isNull(): + pixmap = QPixmap(size, size) + else: + pixmap = QPixmap(size, size) + + if pixmap.width() != size or pixmap.height() != size: + pixmap = pixmap.scaled(size, size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation) + + # Draw a simple circular placeholder if no image + if not image_path: + pixmap.fill(Qt.transparent) + painter = QPainter(pixmap) + painter.setRenderHint(QPainter.Antialiasing) + painter.setBrush(QBrush(QColor("#e0e0e0"))) + painter.setPen(Qt.NoPen) + painter.drawEllipse(0, 0, size, size) + painter.end() + + self.avatar_label.setPixmap(pixmap) + self.avatar_label.setFixedSize(size, size) + self.avatar_label.setScaledContents(True) 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; - }} - """) + if self.is_own: + bg_color = palette["accent"] + text_color = "#ffffff" + timestamp_color = "#e6e6e6" + name_color = palette["text_secondary"] + ts_align = Qt.AlignRight + else: + bg_color = palette["secondary"] + text_color = palette["text"] + timestamp_color = palette["text_secondary"] + name_color = palette["text_secondary"] + ts_align = Qt.AlignLeft + + # Style only the bubble frame + self.bubble_frame.setStyleSheet( + f"#Bubble {{ background-color: {bg_color}; border-radius: 10px; }}" + ) 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 + self.name_label.setStyleSheet(f"color: {name_color}; font-size: 8pt; font-weight: 600;") + + # Keep timestamp alignment in sync with side + self.timestamp_label.setAlignment(ts_align)