""" Compose post dialog for creating new posts """ from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QTextEdit, QPushButton, QLabel, QDialogButtonBox, QCheckBox, QComboBox, QGroupBox ) from PySide6.QtCore import Qt, Signal, QThread from PySide6.QtGui import QKeySequence, QShortcut from accessibility.accessible_combo import AccessibleComboBox from audio.sound_manager import SoundManager from config.settings import SettingsManager from activitypub.client import ActivityPubClient from widgets.autocomplete_textedit import AutocompleteTextEdit class PostThread(QThread): """Background thread for posting content""" post_success = Signal(dict) # Emitted with post data on success post_failed = Signal(str) # Emitted with error message on failure def __init__(self, account, content, visibility, content_warning=None): super().__init__() self.account = account self.content = content self.visibility = visibility self.content_warning = content_warning def run(self): """Post the content in background""" try: client = ActivityPubClient(self.account.instance_url, self.account.access_token) result = client.post_status( content=self.content, visibility=self.visibility, content_warning=self.content_warning ) self.post_success.emit(result) except Exception as e: self.post_failed.emit(str(e)) class ComposeDialog(QDialog): """Dialog for composing new posts""" post_sent = Signal(dict) # Emitted when a post is ready to send def __init__(self, account_manager, parent=None): super().__init__(parent) self.settings = SettingsManager() self.sound_manager = SoundManager(self.settings) self.account_manager = account_manager self.setup_ui() self.setup_shortcuts() def setup_ui(self): """Initialize the compose dialog UI""" self.setWindowTitle("Compose Post") self.setMinimumSize(500, 300) self.setModal(True) layout = QVBoxLayout(self) # Character count label self.char_count_label = QLabel("Characters: 0/500") self.char_count_label.setAccessibleName("Character Count") layout.addWidget(self.char_count_label) # Main text area with autocomplete self.text_edit = AutocompleteTextEdit() self.text_edit.setAccessibleName("Post Content") self.text_edit.setAccessibleDescription("Enter your post content here. Type @ for mentions, : for emojis. Press Tab to move to post options.") self.text_edit.setPlaceholderText("What's on your mind? Type @ for mentions, : for emojis") self.text_edit.setTabChangesFocus(True) # Allow Tab to exit the text area self.text_edit.textChanged.connect(self.update_char_count) self.text_edit.mention_requested.connect(self.load_mention_suggestions) self.text_edit.emoji_requested.connect(self.load_emoji_suggestions) layout.addWidget(self.text_edit) # Options group options_group = QGroupBox("Post Options") options_layout = QVBoxLayout(options_group) # Visibility settings visibility_layout = QHBoxLayout() visibility_layout.addWidget(QLabel("Visibility:")) self.visibility_combo = AccessibleComboBox() self.visibility_combo.setAccessibleName("Post Visibility") self.visibility_combo.addItems([ "Public", "Unlisted", "Followers Only", "Direct Message" ]) visibility_layout.addWidget(self.visibility_combo) visibility_layout.addStretch() options_layout.addLayout(visibility_layout) # Content warnings self.cw_checkbox = QCheckBox("Add Content Warning") self.cw_checkbox.setAccessibleName("Content Warning Toggle") self.cw_checkbox.toggled.connect(self.toggle_content_warning) options_layout.addWidget(self.cw_checkbox) self.cw_edit = QTextEdit() self.cw_edit.setAccessibleName("Content Warning Text") self.cw_edit.setAccessibleDescription("Enter content warning description. Press Tab to move to next field.") self.cw_edit.setPlaceholderText("Describe what this post contains...") self.cw_edit.setMaximumHeight(60) self.cw_edit.setTabChangesFocus(True) # Allow Tab to exit the content warning field self.cw_edit.hide() options_layout.addWidget(self.cw_edit) layout.addWidget(options_group) # Button box button_box = QDialogButtonBox() # Post button self.post_button = QPushButton("&Post") self.post_button.setAccessibleName("Send Post") self.post_button.setDefault(True) self.post_button.clicked.connect(self.send_post) button_box.addButton(self.post_button, QDialogButtonBox.AcceptRole) # Cancel button cancel_button = QPushButton("&Cancel") cancel_button.setAccessibleName("Cancel Post") cancel_button.clicked.connect(self.reject) button_box.addButton(cancel_button, QDialogButtonBox.RejectRole) layout.addWidget(button_box) # Set initial focus self.text_edit.setFocus() def setup_shortcuts(self): """Set up keyboard shortcuts""" # Ctrl+Enter to send post send_shortcut = QShortcut(QKeySequence("Ctrl+Return"), self) send_shortcut.activated.connect(self.send_post) # Escape to cancel cancel_shortcut = QShortcut(QKeySequence.Cancel, self) cancel_shortcut.activated.connect(self.reject) def toggle_content_warning(self, enabled: bool): """Toggle content warning field visibility""" if enabled: self.cw_edit.show() # Don't automatically focus - let user tab to it naturally else: self.cw_edit.hide() self.cw_edit.clear() def update_char_count(self): """Update character count display""" text = self.text_edit.toPlainText() char_count = len(text) self.char_count_label.setText(f"Characters: {char_count}/500") # Enable/disable post button based on content has_content = bool(text.strip()) within_limit = char_count <= 500 self.post_button.setEnabled(has_content and within_limit) # Update accessibility if char_count > 500: self.char_count_label.setAccessibleDescription("Character limit exceeded") else: self.char_count_label.setAccessibleDescription(f"{500 - char_count} characters remaining") def send_post(self): """Send the post""" content = self.text_edit.toPlainText().strip() if not content: return # Get active account active_account = self.account_manager.get_active_account() if not active_account: QMessageBox.warning(self, "No Account", "Please add an account before posting.") return # Get post settings visibility_text = self.visibility_combo.currentText() visibility_map = { "Public": "public", "Unlisted": "unlisted", "Followers Only": "private", "Direct Message": "direct" } visibility = visibility_map.get(visibility_text, "public") content_warning = None if self.cw_checkbox.isChecked(): content_warning = self.cw_edit.toPlainText().strip() # Start background posting post_data = { 'account': active_account, 'content': content, 'visibility': visibility, 'content_warning': content_warning } # Play sound when post button is pressed self.sound_manager.play_event("post") # Emit signal with all post data for background processing self.post_sent.emit(post_data) # Close dialog immediately self.accept() def load_mention_suggestions(self, prefix: str): """Load mention suggestions based on prefix""" # TODO: Implement fetching followers/following from API # For now, use expanded sample suggestions with realistic fediverse usernames sample_mentions = [ "alice", "bob", "charlie", "diana", "eve", "frank", "grace", "henry", "ivy", "jack", "admin", "moderator", "announcements", "news", "updates", "support", "help", "alex_dev", "jane_artist", "mike_writer", "sarah_photographer", "tom_musician", "community", "local_news", "tech_updates", "fedi_tips", "open_source", "cat_lover", "dog_walker", "book_reader", "movie_fan", "game_dev", "web_designer", "climate_activist", "space_enthusiast", "food_blogger", "travel_tales", "art_gallery" ] # Filter by prefix (case insensitive) filtered = [name for name in sample_mentions if name.lower().startswith(prefix.lower())] self.text_edit.update_mention_list(filtered) def load_emoji_suggestions(self, prefix: str): """Load emoji suggestions based on prefix""" # The AutocompleteTextEdit already has a built-in emoji list # This method is called when the signal is emitted, but the # autocomplete logic is handled internally by the text edit widget # We don't need to do anything here since emojis are pre-loaded pass def get_post_data(self) -> dict: """Get the composed post data""" return { 'content': self.text_edit.toPlainText().strip(), 'visibility': self.visibility_combo.currentText().lower().replace(" ", "_"), 'content_warning': self.cw_edit.toPlainText().strip() if self.cw_checkbox.isChecked() else None }