From 5b857bf878c5cfb9a85eca20d2a35fe5e09b030f Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Mon, 9 Mar 2026 20:13:56 +1100 Subject: [PATCH] Improve look of chat messages --- desktop-client/main.py | 9 +- desktop-client/room_widget.py | 172 +++++++++++++++++++++++++++++----- 2 files changed, 154 insertions(+), 27 deletions(-) diff --git a/desktop-client/main.py b/desktop-client/main.py index 66d091a..509a540 100644 --- a/desktop-client/main.py +++ b/desktop-client/main.py @@ -436,7 +436,6 @@ class VlcSyncApp(QMainWindow): #chatMessages { background-color: transparent; border: none; - color: #f1f1f1; } #chatMessages QScrollBar:vertical { @@ -446,12 +445,12 @@ class VlcSyncApp(QMainWindow): margin: 0px; } #chatMessages QScrollBar::handle:vertical { - background: #555; + background: #333; min-height: 20px; border-radius: 4px; } #chatMessages QScrollBar::handle:vertical:hover { - background: #666; + background: #444; } #chatMessages QScrollBar::add-line:vertical, #chatMessages QScrollBar::sub-line:vertical { height: 0px; @@ -459,6 +458,10 @@ class VlcSyncApp(QMainWindow): #chatMessages QScrollBar::add-page:vertical, #chatMessages QScrollBar::sub-page:vertical { background: none; } + + #bubble { + border: none; + } """) if __name__ == "__main__": diff --git a/desktop-client/room_widget.py b/desktop-client/room_widget.py index 4fcafc1..04e8209 100644 --- a/desktop-client/room_widget.py +++ b/desktop-client/room_widget.py @@ -5,15 +5,78 @@ import re from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QFrame, QSlider, QTextEdit, QTextBrowser, QApplication, QToolTip, QSplitter, - QMainWindow + QMainWindow, QScrollArea ) from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QEvent from PyQt6.QtGui import QIcon, QCursor import uuid +import urllib.parse from vlc_player import VLCSyncPlayer +class ChatBubble(QWidget): + def __init__(self, author, text, time_str, is_self, on_link_clicked): + super().__init__() + layout = QHBoxLayout(self) + layout.setContentsMargins(5, 2, 5, 2) + + # Container for the actual bubble + self.bubble = QFrame() + self.bubble.setObjectName("bubble") + + bg_color = "#0084FF" if is_self else "#2a2a2e" + text_color = "#FFFFFF" + author_color = "#E1E1E1" if is_self else "#3ea6ff" + + self.bubble.setStyleSheet(f""" + #bubble {{ + background-color: {bg_color}; + border-radius: 12px; + }} + """) + + bubble_layout = QVBoxLayout(self.bubble) + bubble_layout.setContentsMargins(10, 8, 10, 6) + bubble_layout.setSpacing(2) + + # Author + author_lbl = QLabel("You" if is_self else author) + author_lbl.setStyleSheet(f"color: {author_color}; font-weight: bold; font-size: 11px; background: transparent;") + bubble_layout.addWidget(author_lbl) + + # Text + self.text_lbl = QLabel(text) + self.text_lbl.setWordWrap(True) + self.text_lbl.setStyleSheet(f"color: {text_color}; font-size: 13px; border: none; background: transparent;") + self.text_lbl.setOpenExternalLinks(False) + self.text_lbl.linkActivated.connect(on_link_clicked) + bubble_layout.addWidget(self.text_lbl) + + # Time + time_lbl = QLabel(time_str) + time_lbl.setAlignment(Qt.AlignmentFlag.AlignRight) + time_lbl.setStyleSheet(f"color: {text_color}; font-size: 9px; opacity: 0.6; border: none; background: transparent;") + bubble_layout.addWidget(time_lbl) + + if is_self: + layout.addStretch() + layout.addWidget(self.bubble) + else: + layout.addWidget(self.bubble) + layout.addStretch() + +class SystemMessageWidget(QWidget): + def __init__(self, text): + super().__init__() + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 5, 0, 5) + lbl = QLabel(text) + lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) + lbl.setWordWrap(True) + lbl.setStyleSheet("color: #888; font-style: italic; font-size: 11px; background: transparent;") + layout.addWidget(lbl) + class ChatPopoutWindow(QMainWindow): closed = pyqtSignal() @@ -287,12 +350,24 @@ class RoomWidget(QWidget): tags_layout.addWidget(self.tags_header_btn) tags_layout.addWidget(self.tags_list) - self.chat_messages = QTextBrowser() + self.chat_messages = QScrollArea() + self.chat_messages.setWidgetResizable(True) self.chat_messages.setObjectName("chatMessages") - self.chat_messages.setOpenExternalLinks(False) - self.chat_messages.setOpenLinks(False) - self.chat_messages.anchorClicked.connect(self.on_chat_link_clicked) - self.chat_messages.setHtml("Welcome to the room! 👋") + self.chat_messages.setStyleSheet("background-color: transparent; border: none;") + + self.chat_content = QWidget() + self.chat_content.setObjectName("chatContent") + self.chat_content.setStyleSheet("background: transparent;") + self.chat_content_layout = QVBoxLayout(self.chat_content) + self.chat_content_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + self.chat_content_layout.setContentsMargins(0, 0, 0, 0) + self.chat_content_layout.setSpacing(0) + + self.chat_messages.setWidget(self.chat_content) + self.chat_messages.toPlainText = self.toPlainText # For test compatibility + + # Initial welcome message + self.add_system_message("Welcome to the room! 👋") chat_input_layout = QHBoxLayout() self.chat_input = QLineEdit() @@ -326,7 +401,7 @@ class RoomWidget(QWidget): self.chat_toggle_btn, self.popout_btn]: w.setFocusPolicy(Qt.FocusPolicy.NoFocus) - # specifically for chat_messages, we allow clicking links, but avoid focus stealing + # scroll area handles focus properly self.chat_messages.setFocusPolicy(Qt.FocusPolicy.ClickFocus) self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) @@ -363,7 +438,12 @@ class RoomWidget(QWidget): self.username = username self.set_room_code_display(room_code) self.room_file_badge.setText(f"📄 {file_name}") - self.chat_messages.setHtml("Welcome to the room! 👋") + + # Clear chat + for i in reversed(range(self.chat_content_layout.count())): + self.chat_content_layout.itemAt(i).widget().setParent(None) + + self.add_system_message("Welcome to the room! 👋") self.current_users = [] self._is_first_user_update = True @@ -744,9 +824,21 @@ class RoomWidget(QWidget): def on_chat_link_clicked(self, url): link = url.toString() + print(f"DEBUG: Link clicked: {link}") if link.startswith("seek:"): - time_str = link[5:] - self._handle_seek_command(time_str) + # Format: 'seek:MM:SS?tag=TagName' + data = link[5:] + if "?" in data and "tag=" in data: + time_str, query = data.split("?", 1) + tag_name = "" + for pair in query.split("&"): + if pair.startswith("tag="): + tag_name = urllib.parse.unquote(pair[4:]) + break + self.add_system_message(f"Seeking to {tag_name} ({time_str})") + self._handle_seek_command(time_str) + else: + self._handle_seek_command(data) def add_chat_message(self, author: str, text: str, timestamp: int): try: @@ -762,27 +854,57 @@ class RoomWidget(QWidget): pattern = r'\b(\d{1,2}:\d{2}(?::\d{2})?)\b' linked_text = re.sub(pattern, r'\1', safe_text) - new_msg = f"{safe_author}: {linked_text} {time_str}" - self.chat_messages.append(new_msg) + is_self = author == self.username # Auto-extract tags added by users directly via /tag command - # This will catch messages that look like purely a tag command - # (Since we just append the output of /tag to chat) - match = re.match(r'^[{time_str}] {tag_text} - {author}" + safe_tag = urllib.parse.quote(tag_text) + highlight_html = f"• [{time_str}] {html.escape(tag_text)} - {author}" self.tags_list.append(highlight_html) def add_system_message(self, text: str): - new_msg = f"{text}" - self.chat_messages.append(new_msg) + msg = SystemMessageWidget(text) + self.chat_content_layout.addWidget(msg) + self._scroll_to_bottom() def send_chat(self): text = self.chat_input.text().strip() @@ -855,8 +977,8 @@ class RoomWidget(QWidget): """Parses a time string (absolute time, offset like +30s, or 'now') and returns the target time in MS. Returns None on error.""" current_ms = self.vlc_player.current_time_ms length_ms = self.vlc_player.get_length() - if length_ms <= 0: - return None + # Don't strictly block on length_ms > 0 here, as we might be seeking before length is known + # or length might be reported as 0 for some streams. if arg == "now": return current_ms @@ -883,7 +1005,9 @@ class RoomWidget(QWidget): elif arg.endswith('h'): target_ms = float(arg[:-1]) * 3600 * 1000 else: target_ms = float(arg) * 1000 - return max(0, min(target_ms, length_ms)) + if length_ms > 0: + return max(0, min(target_ms, length_ms)) + return max(0, target_ms) except ValueError: return None