diff --git a/desktop-client/room_widget.py b/desktop-client/room_widget.py
index b36eeeb..0a1987d 100644
--- a/desktop-client/room_widget.py
+++ b/desktop-client/room_widget.py
@@ -1,8 +1,10 @@
import os
import datetime
+import html
+import re
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
- QPushButton, QFrame, QSlider, QTextEdit, QApplication, QToolTip
+ QPushButton, QFrame, QSlider, QTextEdit, QTextBrowser, QApplication, QToolTip
)
from PyQt6.QtCore import Qt, pyqtSignal, QTimer
from PyQt6.QtGui import QIcon
@@ -184,9 +186,11 @@ class RoomWidget(QWidget):
self.users_lbl = QLabel("0 watching")
self.users_lbl.setObjectName("usersLbl")
- self.chat_messages = QTextEdit()
+ self.chat_messages = QTextBrowser()
self.chat_messages.setObjectName("chatMessages")
- self.chat_messages.setReadOnly(True)
+ 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! 👋")
chat_input_layout = QHBoxLayout()
@@ -211,8 +215,11 @@ class RoomWidget(QWidget):
# Prevent UI components from stealing focus (which breaks spacebar shortcuts)
for w in [self.copy_code_btn, self.leave_btn, self.play_btn, self.fullscreen_btn,
- self.chat_send_btn, self.seekbar, self.volume_slider, self.chat_messages]:
+ self.chat_send_btn, self.seekbar, self.volume_slider]:
w.setFocusPolicy(Qt.FocusPolicy.NoFocus)
+
+ # specifically for chat_messages, we allow clicking links, but avoid focus stealing
+ self.chat_messages.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
# Instantiate the VLC Player Wrapper
@@ -296,7 +303,14 @@ class RoomWidget(QWidget):
def keyPressEvent(self, event):
# Allow typing in input fields without triggering shortcuts
focus_widget = QApplication.focusWidget()
- if isinstance(focus_widget, QLineEdit) or isinstance(focus_widget, QTextEdit):
+
+ # Spacebar should always play/pause unless typing in chat input
+ if event.key() == Qt.Key.Key_Space and not isinstance(focus_widget, QLineEdit):
+ self.toggle_playback()
+ event.accept()
+ return
+
+ if isinstance(focus_widget, QLineEdit) or isinstance(focus_widget, QTextEdit) or isinstance(focus_widget, QTextBrowser):
if event.key() == Qt.Key.Key_Escape:
focus_widget.clearFocus()
self.setFocus()
@@ -305,10 +319,7 @@ class RoomWidget(QWidget):
super().keyPressEvent(event)
return
- if event.key() == Qt.Key.Key_Space:
- self.toggle_playback()
- event.accept()
- elif event.key() == Qt.Key.Key_F:
+ if event.key() == Qt.Key.Key_F:
self.toggle_fullscreen()
event.accept()
elif event.key() == Qt.Key.Key_M:
@@ -494,13 +505,27 @@ class RoomWidget(QWidget):
self.current_users = users
self.users_lbl.setText(f"{len(users)} watching: {', '.join(users)}")
+ def on_chat_link_clicked(self, url):
+ link = url.toString()
+ if link.startswith("seek:"):
+ time_str = link[5:]
+ self._handle_seek_command(time_str)
+
def add_chat_message(self, author: str, text: str, timestamp: int):
try:
dt = datetime.datetime.fromtimestamp(timestamp / 1000.0)
except (ValueError, OSError, TypeError):
dt = datetime.datetime.now()
time_str = dt.strftime("%I:%M %p")
- new_msg = f"{author}: {text} {time_str}"
+
+ safe_text = html.escape(text)
+ safe_author = html.escape(author)
+
+ # Regex to find timestamps like 1:23 or 01:23:45
+ 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)
def add_system_message(self, text: str):
@@ -538,9 +563,36 @@ class RoomWidget(QWidget):
else:
self.add_system_message("Usage: /seek [time]")
return
+ elif cmd == "/tag":
+ self.chat_input.setText("")
+ if len(parts) > 1:
+ time_arg = parts[1].lower()
+ tag_name = " ".join(parts[2:])
+
+ if time_arg == "now":
+ current_ms = self.vlc_player.current_time_ms
+ s = max(0, current_ms) // 1000
+ if s >= 3600: time_str = f"{s//3600:02d}:{(s%3600)//60:02d}:{s%60:02d}"
+ else: time_str = f"{(s%3600)//60:02d}:{s%60:02d}"
+ else:
+ time_str = time_arg
+
+ msg = f"{time_str} {tag_name}".strip()
+ self.chat_message_ready.emit(msg)
+ else:
+ self.add_system_message("Usage: /tag [now|time] [name]")
+ return
+ elif cmd == "/time":
+ self.chat_input.setText("")
+ current_ms = self.vlc_player.current_time_ms
+ s = max(0, current_ms) // 1000
+ if s >= 3600: time_str = f"{s//3600:02d}:{(s%3600)//60:02d}:{s%60:02d}"
+ else: time_str = f"{(s%3600)//60:02d}:{s%60:02d}"
+ self.add_system_message(f"Current time: {time_str}")
+ return
elif cmd == "/help":
self.chat_input.setText("")
- self.add_system_message("Available commands:
/play - Resume playback
/pause - Pause playback
/seek [time] - Seek to specific time (e.g., 1:23) or offset (e.g., +30s, -1m)
/help - Show this message")
+ self.add_system_message("Available commands:
/play - Resume playback
/pause - Pause playback
/seek [time] - Seek to specific time
/tag [now|time] [name] - Tag a timestamp
/time - Show current time
/help - Show this message")
return
self.chat_message_ready.emit(text)