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