From 777e08ff8584955a8ddaed4f65a8205297d670b4 Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Wed, 4 Mar 2026 23:25:57 +1100 Subject: [PATCH] Add /tag & /time command to allow users to sending clickable timestamps in chat --- desktop-client/room_widget.py | 74 +++++++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 11 deletions(-) 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)