""" Post details dialog showing favorites, boosts, and other interaction details """ from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QLabel, QTextEdit, QTabWidget, QListWidget, QListWidgetItem, QDialogButtonBox, QWidget, QGroupBox, QPushButton, QCheckBox, QRadioButton, QButtonGroup, ) from PySide6.QtCore import Qt, Signal, QThread from PySide6.QtGui import QFont from typing import List, Dict, Any, Optional import logging from activitypub.client import ActivityPubClient from models.user import User from audio.sound_manager import SoundManager class FetchDetailsThread(QThread): """Background thread for fetching post interaction details""" details_loaded = Signal(dict) # Emitted with details data details_failed = Signal(str) # Emitted with error message def __init__(self, client: ActivityPubClient, post_id: str): super().__init__() self.client = client self.post_id = post_id self.logger = logging.getLogger("bifrost.post_details") def run(self): """Fetch favorites and boosts in background""" try: details = {"favourited_by": [], "reblogged_by": []} # Fetch who favorited this post try: favourited_by_data = self.client.get_status_favourited_by(self.post_id) details["favourited_by"] = favourited_by_data except Exception as e: self.logger.error(f"Failed to fetch favorites: {e}") # Fetch who boosted this post try: reblogged_by_data = self.client.get_status_reblogged_by(self.post_id) details["reblogged_by"] = reblogged_by_data except Exception as e: self.logger.error(f"Failed to fetch boosts: {e}") self.details_loaded.emit(details) except Exception as e: self.details_failed.emit(str(e)) class PostDetailsDialog(QDialog): """Dialog showing detailed post interaction information""" vote_submitted = Signal( object, list ) # Emitted with post and list of selected choice indices def __init__( self, post, client: ActivityPubClient, sound_manager: SoundManager, parent=None ): super().__init__(parent) self.post = post self.client = client self.sound_manager = sound_manager self.logger = logging.getLogger("bifrost.post_details") self.setWindowTitle("Post Details") self.setModal(True) self.resize(600, 500) self.setup_ui() self.load_details() def setup_ui(self): """Setup the post details UI""" layout = QVBoxLayout(self) # Post statistics (keep just the basic stats at top) stats_text = f"Replies: {self.post.replies_count} | Boosts: {self.post.reblogs_count} | Favorites: {self.post.favourites_count}" stats_label = QLabel(stats_text) stats_label.setAccessibleName("Post Statistics") layout.addWidget(stats_label) # Tabs for interaction details self.tabs = QTabWidget() self.tabs.setAccessibleName("Interaction Details") # Poll tab (if poll exists) - add as first tab if hasattr(self.post, "poll") and self.post.poll: self.poll_widget = self.create_poll_widget() poll_tab_index = self.tabs.addTab(self.poll_widget, "Poll") self.logger.debug(f"Added poll tab at index {poll_tab_index}") # Content tab - always present self.content_widget = self.create_content_widget() content_tab_index = self.tabs.addTab(self.content_widget, "Content") self.logger.debug(f"Added content tab at index {content_tab_index}") # Favorites tab self.favorites_list = QListWidget() self.favorites_list.setAccessibleName("Users Who Favorited") # Add fake header for single-item navigation fake_header = QListWidgetItem("Users who favorited this post:") fake_header.setFlags(Qt.ItemIsEnabled) # Not selectable self.favorites_list.addItem(fake_header) self.tabs.addTab( self.favorites_list, f"Favorites ({self.post.favourites_count})" ) # Boosts tab self.boosts_list = QListWidget() self.boosts_list.setAccessibleName("Users Who Boosted") # Add fake header for single-item navigation fake_header = QListWidgetItem("Users who boosted this post:") fake_header.setFlags(Qt.ItemIsEnabled) # Not selectable self.boosts_list.addItem(fake_header) self.tabs.addTab(self.boosts_list, f"Boosts ({self.post.reblogs_count})") layout.addWidget(self.tabs) # Loading indicator self.status_label = QLabel("Loading interaction details...") self.status_label.setAccessibleName("Loading Status") layout.addWidget(self.status_label) # Button box button_box = QDialogButtonBox(QDialogButtonBox.Close) button_box.setAccessibleName("Dialog Buttons") button_box.rejected.connect(self.reject) layout.addWidget(button_box) def load_details(self): """Load detailed interaction information""" if not self.client or not hasattr(self.post, "id"): self.status_label.setText("Cannot load details: No post ID or API client") return # Start background fetch self.fetch_thread = FetchDetailsThread(self.client, self.post.id) self.fetch_thread.details_loaded.connect(self.on_details_loaded) self.fetch_thread.details_failed.connect(self.on_details_failed) self.fetch_thread.start() def on_details_loaded(self, details: dict): """Handle successful details loading""" self.status_label.setText("") # Populate favorites list favourited_by = details.get("favourited_by", []) if favourited_by: for account_data in favourited_by: try: user = User.from_api_dict(account_data) display_name = user.display_name or user.username item_text = f"@{user.username} ({display_name})" item = QListWidgetItem(item_text) item.setData(Qt.UserRole, user) self.favorites_list.addItem(item) except Exception as e: self.logger.error(f"Error parsing favorite user: {e}") else: item = QListWidgetItem("No one has favorited this post yet") self.favorites_list.addItem(item) # Populate boosts list reblogged_by = details.get("reblogged_by", []) if reblogged_by: for account_data in reblogged_by: try: user = User.from_api_dict(account_data) display_name = user.display_name or user.username item_text = f"@{user.username} ({display_name})" item = QListWidgetItem(item_text) item.setData(Qt.UserRole, user) self.boosts_list.addItem(item) except Exception as e: self.logger.error(f"Error parsing boost user: {e}") else: item = QListWidgetItem("No one has boosted this post yet") self.boosts_list.addItem(item) # Update tab titles with actual counts actual_favorites = len(favourited_by) actual_boosts = len(reblogged_by) # Account for poll tab if it exists # Tab order: Poll (if exists), Content, Favorites, Boosts has_poll = hasattr(self.post, "poll") and self.post.poll favorites_tab_index = 2 if has_poll else 1 boosts_tab_index = 3 if has_poll else 2 self.tabs.setTabText(favorites_tab_index, f"Favorites ({actual_favorites})") self.tabs.setTabText(boosts_tab_index, f"Boosts ({actual_boosts})") # Play success sound self.sound_manager.play_success() def on_details_failed(self, error_message: str): """Handle details loading failure""" self.status_label.setText(f"Failed to load details: {error_message}") # Add error items to lists error_item_fav = QListWidgetItem(f"Error loading favorites: {error_message}") self.favorites_list.addItem(error_item_fav) error_item_boost = QListWidgetItem(f"Error loading boosts: {error_message}") self.boosts_list.addItem(error_item_boost) # Play error sound self.sound_manager.play_error() def create_poll_widget(self): """Create poll voting widget for the poll tab""" poll_widget = QWidget() poll_layout = QVBoxLayout(poll_widget) poll_data = self.post.poll # Poll question (if exists) if "question" in poll_data and poll_data["question"]: question_label = QLabel(f"Question: {poll_data['question']}") question_label.setAccessibleName("Poll Question") question_label.setWordWrap(True) poll_layout.addWidget(question_label) # Check if user can still vote can_vote = not poll_data.get("voted", False) and not poll_data.get( "expired", False ) if can_vote: # Show interactive voting interface self.poll_results_list = self.create_interactive_poll_widget( poll_data, poll_layout ) else: # Show results as accessible list (like favorites/boosts) self.poll_results_list = self.create_poll_results_list(poll_data) poll_layout.addWidget(self.poll_results_list) # Poll info info_text = [] if "expires_at" in poll_data and poll_data["expires_at"]: info_text.append(f"Expires: {poll_data['expires_at']}") if "voters_count" in poll_data: info_text.append(f"Total voters: {poll_data['voters_count']}") if info_text: info_label = QLabel(" | ".join(info_text)) info_label.setAccessibleName("Poll Information") poll_layout.addWidget(info_label) # Status message if poll_data.get("voted", False): voted_label = QLabel("✓ You have already voted in this poll") voted_label.setAccessibleName("Vote Status") poll_layout.addWidget(voted_label) elif poll_data.get("expired", False): expired_label = QLabel("This poll has expired") expired_label.setAccessibleName("Poll Status") poll_layout.addWidget(expired_label) return poll_widget def create_poll_results_list(self, poll_data): """Create accessible list widget for poll results (expired/voted polls)""" results_list = QListWidget() results_list.setAccessibleName("Poll Results") # Add fake header for single-item navigation (like favorites/boosts) fake_header = QListWidgetItem("Poll results:") fake_header.setFlags(Qt.ItemIsEnabled) # Not selectable results_list.addItem(fake_header) # Add poll options with results options = poll_data.get("options", []) own_votes = poll_data.get("own_votes", []) for i, option in enumerate(options): vote_count = option.get("votes_count", 0) option_title = option.get("title", f"Option {i + 1}") # Mark user's votes vote_indicator = " ✓" if i in own_votes else "" option_text = f"{option_title}: {vote_count} votes{vote_indicator}" item = QListWidgetItem(option_text) item.setData(Qt.UserRole, {"option_index": i, "option_data": option}) results_list.addItem(item) return results_list def create_interactive_poll_widget(self, poll_data, poll_layout): """Create interactive poll widget for active polls""" # Poll options group options_group = QGroupBox("Poll Options") options_group.setAccessibleName("Poll Options") options_layout = QVBoxLayout(options_group) self.poll_option_widgets = [] self.poll_button_group = None # Check if poll allows multiple choices multiple_choice = poll_data.get("multiple", False) if not multiple_choice: # Single choice - use radio buttons self.poll_button_group = QButtonGroup() # Add options options = poll_data.get("options", []) for i, option in enumerate(options): vote_count = option.get("votes_count", 0) option_title = option.get("title", f"Option {i + 1}") option_text = f"{option_title} ({vote_count} votes)" if multiple_choice: # Multiple choice - use checkboxes option_widget = QCheckBox(option_text) else: # Single choice - use radio buttons option_widget = QRadioButton(option_text) self.poll_button_group.addButton(option_widget, i) option_widget.setAccessibleName(f"Poll Option {i + 1}") self.poll_option_widgets.append(option_widget) options_layout.addWidget(option_widget) poll_layout.addWidget(options_group) # Vote button vote_button = QPushButton("Submit Vote") vote_button.setAccessibleName("Submit Poll Vote") vote_button.clicked.connect(self.submit_poll_vote) poll_layout.addWidget(vote_button) return None # No list widget for interactive polls def create_content_widget(self): """Create content widget for the content tab""" content_widget = QWidget() content_layout = QVBoxLayout(content_widget) # Create comprehensive post content with all details in accessible text box content_parts = [] # Author info if hasattr(self.post, "account") and self.post.account: username = getattr(self.post.account, "username", "unknown") display_name = getattr(self.post.account, "display_name", "") or username content_parts.append(f"Author: @{username} ({display_name})") else: content_parts.append("Author: Information not available") content_parts.append("") # Empty line separator # Post content post_content = self.post.get_content_text() if post_content.strip(): content_parts.append("Content:") content_parts.append(post_content) else: content_parts.append("Content: (No text content)") content_parts.append("") # Empty line separator # Post metadata metadata_parts = [] if hasattr(self.post, "created_at") and self.post.created_at: metadata_parts.append(f"Posted: {self.post.created_at}") if hasattr(self.post, "visibility") and self.post.visibility: metadata_parts.append(f"Visibility: {self.post.visibility}") if hasattr(self.post, "language") and self.post.language: metadata_parts.append(f"Language: {self.post.language}") if metadata_parts: content_parts.append("Post Details:") content_parts.extend(metadata_parts) # Combine all parts into one accessible text widget full_content = "\n".join(content_parts) # Post content text (scrollable) with all details content_text = QTextEdit() content_text.setAccessibleName("Full Post Details") content_text.setPlainText(full_content) content_text.setReadOnly(True) # Enable keyboard navigation in read-only text content_text.setTextInteractionFlags( Qt.TextSelectableByKeyboard | Qt.TextSelectableByMouse ) content_layout.addWidget(content_text) return content_widget def submit_poll_vote(self): """Submit vote in poll""" try: selected_choices = [] for i, widget in enumerate(self.poll_option_widgets): if widget.isChecked(): selected_choices.append(i) if not selected_choices: self.sound_manager.play_error() return # Emit vote signal for parent to handle self.vote_submitted.emit(self.post, selected_choices) # Close dialog after voting self.accept() except Exception as e: self.logger.error(f"Error submitting poll vote: {e}") self.sound_manager.play_error()