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, 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) bubble_layout.addWidget(self.text_label) bubble_layout.addWidget(self.timestamp_label, 0, Qt.AlignRight if self.is_own else Qt.AlignLeft) 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 = "#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;") 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)