diff --git a/src/main_window.py b/src/main_window.py index 4f1b0ef..64924c6 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -29,6 +29,7 @@ from widgets.soundpack_manager_dialog import SoundpackManagerDialog from widgets.profile_dialog import ProfileDialog from widgets.accessible_text_dialog import AccessibleTextDialog from managers.post_manager import PostManager +from managers.post_actions_manager import PostActionsManager from managers.sound_coordinator import SoundCoordinator from managers.error_manager import ErrorManager @@ -67,6 +68,12 @@ class MainWindow(QMainWindow): self.post_manager = PostManager(self.account_manager, timeline_sound_manager) self.post_manager.post_success.connect(self.on_post_success) self.post_manager.post_failed.connect(self.on_post_failed) + + # Post actions management - single point of truth for boost/favorite/etc + self.post_actions_manager = PostActionsManager(self.account_manager, timeline_sound_manager) + self.post_actions_manager.action_success.connect(self.on_action_success) + self.post_actions_manager.action_failed.connect(self.on_action_failed) + self.post_actions_manager.refresh_requested.connect(self.on_action_refresh_requested) self.setup_menus() self.setup_shortcuts() @@ -728,6 +735,18 @@ class MainWindow(QMainWindow): self.error_manager.handle_api_error( f"Post failed: {error_message}", context="post_creation" ) + + def on_action_success(self, action_type: str, message: str): + """Handle successful post actions from centralized manager""" + self.status_bar.showMessage(message, 2000) + + def on_action_failed(self, action_type: str, error_message: str): + """Handle failed post actions from centralized manager""" + self.status_bar.showMessage(error_message, 3000) + + def on_action_refresh_requested(self, action_type: str): + """Handle timeline refresh requests from post actions""" + self.timeline.request_post_action_refresh(action_type) def show_settings(self): """Show the settings dialog""" @@ -1017,54 +1036,12 @@ class MainWindow(QMainWindow): self.post_manager._handle_post_failed(str(e)) def boost_post(self, post): - """Boost/unboost a post""" - active_account = self.account_manager.get_active_account() - if not active_account: - return - - try: - client = self.account_manager.get_client_for_active_account() - if not client: - return - if post.reblogged: - client.unreblog_status(post.id) - self.status_bar.showMessage("Post unboosted", 2000) - else: - client.reblog_status(post.id) - self.status_bar.showMessage("Post boosted", 2000) - # Play boost sound for successful boost - if hasattr(self.timeline, "sound_manager"): - self.logger.debug("Playing boost sound for user boost action") - self.timeline.sound_manager.play_boost() - # Refresh timeline to show updated state - self.timeline.request_post_action_refresh("boost") - except Exception as e: - self.status_bar.showMessage(f"Boost failed: {str(e)}", 3000) + """Boost/unboost a post - USING CENTRALIZED POST ACTIONS MANAGER""" + self.post_actions_manager.boost_post(post) def favorite_post(self, post): - """Favorite/unfavorite a post""" - active_account = self.account_manager.get_active_account() - if not active_account: - return - - try: - client = self.account_manager.get_client_for_active_account() - if not client: - return - if post.favourited: - client.unfavourite_status(post.id) - self.status_bar.showMessage("Post unfavorited", 2000) - else: - client.favourite_status(post.id) - self.status_bar.showMessage("Post favorited", 2000) - # Play favorite sound for successful favorite - if hasattr(self.timeline, "sound_manager"): - self.logger.debug("Playing favorite sound for user favorite action") - self.timeline.sound_manager.play_favorite() - # Refresh timeline to show updated state - self.timeline.request_post_action_refresh("favorite") - except Exception as e: - self.status_bar.showMessage(f"Favorite failed: {str(e)}", 3000) + """Favorite/unfavorite a post - USING CENTRALIZED POST ACTIONS MANAGER""" + self.post_actions_manager.favorite_post(post) def view_profile(self, post): """View user profile""" @@ -1171,68 +1148,13 @@ class MainWindow(QMainWindow): self.status_bar.showMessage("No post selected", 2000) def delete_post(self, post): - """Delete a post with confirmation dialog""" - from PySide6.QtWidgets import QMessageBox - - # Check if this is user's own post - active_account = self.account_manager.get_active_account() - if not active_account or not hasattr(post, "account"): - self.status_bar.showMessage("Cannot delete: No active account", 2000) - return - - is_own_post = ( - post.account.username == active_account.username - and post.account.acct.split("@")[-1] - == active_account.instance_url.replace("https://", "").replace( - "http://", "" - ) - ) - - if not is_own_post: - self.status_bar.showMessage("Cannot delete: Not your post", 2000) - return - - # Show confirmation dialog - content_preview = post.get_content_text() - - result = QMessageBox.question( - self, - "Delete Post", - f'Are you sure you want to delete this post?\n\n"{content_preview}"', - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No, - ) - - if result == QMessageBox.Yes: - try: - client = self.account_manager.get_client_for_active_account() - if not client: - return - client.delete_status(post.id) - self.status_bar.showMessage("Post deleted successfully", 2000) - # Refresh timeline to remove deleted post - self.timeline.request_post_action_refresh("delete") - except Exception as e: - self.status_bar.showMessage(f"Delete failed: {str(e)}", 3000) + """Delete a post with confirmation dialog - USING CENTRALIZED POST ACTIONS MANAGER""" + self.post_actions_manager.delete_post(post, parent_widget=self) def edit_post(self, post): - """Edit a post""" - # Check if this is user's own post - active_account = self.account_manager.get_active_account() - if not active_account or not hasattr(post, "account"): - self.status_bar.showMessage("Cannot edit: No active account", 2000) - return - - is_own_post = ( - post.account.username == active_account.username - and post.account.acct.split("@")[-1] - == active_account.instance_url.replace("https://", "").replace( - "http://", "" - ) - ) - - if not is_own_post: - self.status_bar.showMessage("Cannot edit: Not your post", 2000) + """Edit a post - USING CENTRALIZED POST ACTIONS MANAGER""" + # Check permissions using centralized manager + if not self.post_actions_manager.edit_post(post, parent_widget=self): return # Open compose dialog with current post content @@ -1244,65 +1166,25 @@ class MainWindow(QMainWindow): dialog.text_edit.setTextCursor(cursor) def handle_edit_sent(data): - try: - client = self.account_manager.get_client_for_active_account() - if not client: - return - client.edit_status( - post.id, - content=data["content"], - visibility=data["visibility"], - content_type=data.get("content_type", "text/plain"), - content_warning=data["content_warning"], - ) - self.status_bar.showMessage("Post edited successfully", 2000) - # Refresh timeline to show edited post - self.timeline.request_post_action_refresh("edit") - except Exception as e: - self.status_bar.showMessage(f"Edit failed: {str(e)}", 3000) + # Use centralized edit operation + self.post_actions_manager.perform_edit( + post, + new_content=data["content"], + visibility=data["visibility"], + content_type=data.get("content_type", "text/plain"), + content_warning=data["content_warning"] + ) dialog.post_sent.connect(handle_edit_sent) dialog.exec() def follow_user(self, post): - """Follow a user""" - active_account = self.account_manager.get_active_account() - if not active_account or not hasattr(post, "account"): - self.status_bar.showMessage("Cannot follow: No active account", 2000) - return - - try: - client = self.account_manager.get_client_for_active_account() - if not client: - return - client.follow_account(post.account.id) - username = post.account.display_name or post.account.username - self.status_bar.showMessage(f"Followed {username}", 2000) - # Play follow sound for successful follow - if hasattr(self.timeline, "sound_manager"): - self.timeline.sound_manager.play_follow() - except Exception as e: - self.status_bar.showMessage(f"Follow failed: {str(e)}", 3000) + """Follow a user - USING CENTRALIZED POST ACTIONS MANAGER""" + self.post_actions_manager.follow_user(post) def unfollow_user(self, post): - """Unfollow a user""" - active_account = self.account_manager.get_active_account() - if not active_account or not hasattr(post, "account"): - self.status_bar.showMessage("Cannot unfollow: No active account", 2000) - return - - try: - client = self.account_manager.get_client_for_active_account() - if not client: - return - client.unfollow_account(post.account.id) - username = post.account.display_name or post.account.username - self.status_bar.showMessage(f"Unfollowed {username}", 2000) - # Play unfollow sound for successful unfollow - if hasattr(self.timeline, "sound_manager"): - self.timeline.sound_manager.play_unfollow() - except Exception as e: - self.status_bar.showMessage(f"Unfollow failed: {str(e)}", 3000) + """Unfollow a user - USING CENTRALIZED POST ACTIONS MANAGER""" + self.post_actions_manager.unfollow_user(post) def show_manual_follow_dialog(self): """Show dialog to manually follow a user by @username@instance""" @@ -1492,87 +1374,12 @@ class MainWindow(QMainWindow): self.status_bar.showMessage("No post selected", 2000) def block_user(self, post): - """Block a user with confirmation dialog""" - active_account = self.account_manager.get_active_account() - if not active_account or not hasattr(post, "account"): - self.status_bar.showMessage("Cannot block: No active account", 2000) - return - - # Don't allow blocking yourself - is_own_post = ( - post.account.username == active_account.username - and post.account.acct.split("@")[-1] - == active_account.instance_url.replace("https://", "").replace( - "http://", "" - ) - ) - - if is_own_post: - self.status_bar.showMessage("Cannot block: Cannot block yourself", 2000) - return - - # Show confirmation dialog - username = post.account.display_name or post.account.username - full_username = f"@{post.account.acct}" - - result = QMessageBox.question( - self, - "Block User", - f"Are you sure you want to block {username} ({full_username})?\n\n" - "This will prevent them from following you and seeing your posts.", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No, - ) - - if result == QMessageBox.Yes: - try: - client = self.account_manager.get_client_for_active_account() - if not client: - return - client.block_account(post.account.id) - self.status_bar.showMessage(f"Blocked {username}", 2000) - # Play success sound for successful block - if hasattr(self.timeline, "sound_manager"): - self.timeline.sound_manager.play_success() - except Exception as e: - self.status_bar.showMessage(f"Block failed: {str(e)}", 3000) - if hasattr(self.timeline, "sound_manager"): - self.timeline.sound_manager.play_error() + """Block a user with confirmation dialog - USING CENTRALIZED POST ACTIONS MANAGER""" + self.post_actions_manager.block_user(post, parent_widget=self) def mute_user(self, post): - """Mute a user""" - active_account = self.account_manager.get_active_account() - if not active_account or not hasattr(post, "account"): - self.status_bar.showMessage("Cannot mute: No active account", 2000) - return - - # Don't allow muting yourself - is_own_post = ( - post.account.username == active_account.username - and post.account.acct.split("@")[-1] - == active_account.instance_url.replace("https://", "").replace( - "http://", "" - ) - ) - - if is_own_post: - self.status_bar.showMessage("Cannot mute: Cannot mute yourself", 2000) - return - - try: - client = self.account_manager.get_client_for_active_account() - if not client: - return - client.mute_account(post.account.id) - username = post.account.display_name or post.account.username - self.status_bar.showMessage(f"Muted {username}", 2000) - # Play success sound for successful mute - if hasattr(self.timeline, "sound_manager"): - self.timeline.sound_manager.play_success() - except Exception as e: - self.status_bar.showMessage(f"Mute failed: {str(e)}", 3000) - if hasattr(self.timeline, "sound_manager"): - self.timeline.sound_manager.play_error() + """Mute a user - USING CENTRALIZED POST ACTIONS MANAGER""" + self.post_actions_manager.mute_user(post) def closeEvent(self, event): """Handle window close event""" diff --git a/src/managers/post_actions_manager.py b/src/managers/post_actions_manager.py new file mode 100644 index 0000000..161c2a6 --- /dev/null +++ b/src/managers/post_actions_manager.py @@ -0,0 +1,397 @@ +""" +PostActionsManager - Single Point of Truth for all post interaction actions +Centralizes boost, favorite, follow, block, mute, delete, and edit operations +""" + +from PySide6.QtCore import QObject, Signal +from PySide6.QtWidgets import QMessageBox +from typing import Optional +import logging + +from config.accounts import AccountManager +from audio.sound_manager import SoundManager + + +class PostActionsManager(QObject): + """Single Point of Truth for all post interaction operations""" + + # Signals for UI coordination + action_success = Signal(str, str) # action_type, message + action_failed = Signal(str, str) # action_type, error_message + refresh_requested = Signal(str) # action_type (for timeline refresh) + + def __init__(self, account_manager: AccountManager, sound_manager: Optional[SoundManager] = None): + super().__init__() + self.account_manager = account_manager + self.sound_manager = sound_manager + self.logger = logging.getLogger("bifrost.post_actions") + + def boost_post(self, post) -> bool: + """Boost/unboost a post - SINGLE POINT OF TRUTH""" + active_account = self.account_manager.get_active_account() + if not active_account: + self.action_failed.emit("boost", "No active account") + return False + + try: + client = self.account_manager.get_client_for_active_account() + if not client: + self.action_failed.emit("boost", "No client connection") + return False + + if post.reblogged: + client.unreblog_status(post.id) + self.action_success.emit("boost", "Post unboosted") + else: + client.reblog_status(post.id) + self.action_success.emit("boost", "Post boosted") + # Play boost sound for successful boost + if self.sound_manager: + self.logger.debug("Playing boost sound for user boost action") + self.sound_manager.play_boost() + + # Request timeline refresh + self.refresh_requested.emit("boost") + return True + + except Exception as e: + error_msg = f"Boost failed: {str(e)}" + self.action_failed.emit("boost", error_msg) + return False + + def favorite_post(self, post) -> bool: + """Favorite/unfavorite a post - SINGLE POINT OF TRUTH""" + active_account = self.account_manager.get_active_account() + if not active_account: + self.action_failed.emit("favorite", "No active account") + return False + + try: + client = self.account_manager.get_client_for_active_account() + if not client: + self.action_failed.emit("favorite", "No client connection") + return False + + if post.favourited: + client.unfavourite_status(post.id) + self.action_success.emit("favorite", "Post unfavorited") + else: + client.favourite_status(post.id) + self.action_success.emit("favorite", "Post favorited") + # Play favorite sound for successful favorite + if self.sound_manager: + self.logger.debug("Playing favorite sound for user favorite action") + self.sound_manager.play_favorite() + + # Request timeline refresh + self.refresh_requested.emit("favorite") + return True + + except Exception as e: + error_msg = f"Favorite failed: {str(e)}" + self.action_failed.emit("favorite", error_msg) + return False + + def follow_user(self, post) -> bool: + """Follow a user - SINGLE POINT OF TRUTH""" + active_account = self.account_manager.get_active_account() + if not active_account or not hasattr(post, "account"): + self.action_failed.emit("follow", "No active account or invalid post") + return False + + try: + client = self.account_manager.get_client_for_active_account() + if not client: + self.action_failed.emit("follow", "No client connection") + return False + + client.follow_account(post.account.id) + username = post.account.display_name or post.account.username + self.action_success.emit("follow", f"Followed {username}") + + # Play follow sound for successful follow + if self.sound_manager: + self.sound_manager.play_follow() + return True + + except Exception as e: + error_msg = f"Follow failed: {str(e)}" + self.action_failed.emit("follow", error_msg) + return False + + def unfollow_user(self, post) -> bool: + """Unfollow a user - SINGLE POINT OF TRUTH""" + active_account = self.account_manager.get_active_account() + if not active_account or not hasattr(post, "account"): + self.action_failed.emit("unfollow", "No active account or invalid post") + return False + + try: + client = self.account_manager.get_client_for_active_account() + if not client: + self.action_failed.emit("unfollow", "No client connection") + return False + + client.unfollow_account(post.account.id) + username = post.account.display_name or post.account.username + self.action_success.emit("unfollow", f"Unfollowed {username}") + + # Play unfollow sound for successful unfollow + if self.sound_manager: + self.sound_manager.play_unfollow() + return True + + except Exception as e: + error_msg = f"Unfollow failed: {str(e)}" + self.action_failed.emit("unfollow", error_msg) + return False + + def delete_post(self, post, parent_widget=None) -> bool: + """Delete a post with confirmation - SINGLE POINT OF TRUTH""" + active_account = self.account_manager.get_active_account() + if not active_account or not hasattr(post, "account"): + self.action_failed.emit("delete", "No active account") + return False + + # Check if this is user's own post + is_own_post = ( + post.account.username == active_account.username + and post.account.acct.split("@")[-1] + == active_account.instance_url.replace("https://", "").replace("http://", "") + ) + + if not is_own_post: + self.action_failed.emit("delete", "Cannot delete: Not your post") + return False + + # Show confirmation dialog + content_preview = post.get_content_text() + result = QMessageBox.question( + parent_widget, + "Delete Post", + f'Are you sure you want to delete this post?\n\n"{content_preview}"', + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No, + ) + + if result == QMessageBox.Yes: + try: + client = self.account_manager.get_client_for_active_account() + if not client: + self.action_failed.emit("delete", "No client connection") + return False + + client.delete_status(post.id) + self.action_success.emit("delete", "Post deleted successfully") + + # Request timeline refresh + self.refresh_requested.emit("delete") + return True + + except Exception as e: + error_msg = f"Delete failed: {str(e)}" + self.action_failed.emit("delete", error_msg) + return False + + return False + + def block_user(self, post, parent_widget=None) -> bool: + """Block a user with confirmation - SINGLE POINT OF TRUTH""" + active_account = self.account_manager.get_active_account() + if not active_account or not hasattr(post, "account"): + self.action_failed.emit("block", "No active account") + return False + + # Don't allow blocking yourself + is_own_post = ( + post.account.username == active_account.username + and post.account.acct.split("@")[-1] + == active_account.instance_url.replace("https://", "").replace("http://", "") + ) + + if is_own_post: + self.action_failed.emit("block", "Cannot block yourself") + return False + + # Show confirmation dialog + username = post.account.display_name or post.account.username + full_username = f"@{post.account.acct}" + + result = QMessageBox.question( + parent_widget, + "Block User", + f"Are you sure you want to block {username} ({full_username})?\n\n" + "This will prevent them from following you and seeing your posts.", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No, + ) + + if result == QMessageBox.Yes: + try: + client = self.account_manager.get_client_for_active_account() + if not client: + self.action_failed.emit("block", "No client connection") + return False + + client.block_account(post.account.id) + self.action_success.emit("block", f"Blocked {username}") + + # Play success sound for successful block + if self.sound_manager: + self.sound_manager.play_success() + return True + + except Exception as e: + error_msg = f"Block failed: {str(e)}" + self.action_failed.emit("block", error_msg) + if self.sound_manager: + self.sound_manager.play_error() + return False + + return False + + def mute_user(self, post) -> bool: + """Mute a user - SINGLE POINT OF TRUTH""" + active_account = self.account_manager.get_active_account() + if not active_account or not hasattr(post, "account"): + self.action_failed.emit("mute", "No active account") + return False + + # Don't allow muting yourself + is_own_post = ( + post.account.username == active_account.username + and post.account.acct.split("@")[-1] + == active_account.instance_url.replace("https://", "").replace("http://", "") + ) + + if is_own_post: + self.action_failed.emit("mute", "Cannot mute yourself") + return False + + try: + client = self.account_manager.get_client_for_active_account() + if not client: + self.action_failed.emit("mute", "No client connection") + return False + + client.mute_account(post.account.id) + username = post.account.display_name or post.account.username + self.action_success.emit("mute", f"Muted {username}") + + # Play success sound for successful mute + if self.sound_manager: + self.sound_manager.play_success() + return True + + except Exception as e: + error_msg = f"Mute failed: {str(e)}" + self.action_failed.emit("mute", error_msg) + if self.sound_manager: + self.sound_manager.play_error() + return False + + def edit_post(self, post, parent_widget=None) -> bool: + """Check if post can be edited and emit signal for UI handling - SINGLE POINT OF TRUTH""" + active_account = self.account_manager.get_active_account() + if not active_account or not hasattr(post, "account"): + self.action_failed.emit("edit", "No active account") + return False + + # Check if this is user's own post + is_own_post = ( + post.account.username == active_account.username + and post.account.acct.split("@")[-1] + == active_account.instance_url.replace("https://", "").replace("http://", "") + ) + + if not is_own_post: + self.action_failed.emit("edit", "Cannot edit: Not your post") + return False + + # Edit handling is delegated to UI layer since it involves compose dialog + # This method just validates permissions + return True + + def perform_edit(self, post, new_content: str, visibility: str, content_type: str = "text/plain", content_warning: str = None) -> bool: + """Perform the actual edit operation - SINGLE POINT OF TRUTH""" + try: + client = self.account_manager.get_client_for_active_account() + if not client: + self.action_failed.emit("edit", "No client connection") + return False + + client.edit_status( + post.id, + content=new_content, + visibility=visibility, + content_type=content_type, + content_warning=content_warning, + ) + self.action_success.emit("edit", "Post edited successfully") + + # Request timeline refresh + self.refresh_requested.emit("edit") + return True + + except Exception as e: + error_msg = f"Edit failed: {str(e)}" + self.action_failed.emit("edit", error_msg) + return False + + def unblock_user(self, post) -> bool: + """Unblock a user - SINGLE POINT OF TRUTH""" + active_account = self.account_manager.get_active_account() + if not active_account or not hasattr(post, "account"): + self.action_failed.emit("unblock", "No active account") + return False + + try: + client = self.account_manager.get_client_for_active_account() + if not client: + self.action_failed.emit("unblock", "No client connection") + return False + + client.unblock_account(post.account.id) + username = post.account.display_name or post.account.username + self.action_success.emit("unblock", f"Unblocked {username}") + + # Play success sound for successful unblock + if self.sound_manager: + self.sound_manager.play_success() + return True + + except Exception as e: + error_msg = f"Unblock failed: {str(e)}" + self.action_failed.emit("unblock", error_msg) + if self.sound_manager: + self.sound_manager.play_error() + return False + + def unmute_user(self, post) -> bool: + """Unmute a user - SINGLE POINT OF TRUTH""" + active_account = self.account_manager.get_active_account() + if not active_account or not hasattr(post, "account"): + self.action_failed.emit("unmute", "No active account") + return False + + try: + client = self.account_manager.get_client_for_active_account() + if not client: + self.action_failed.emit("unmute", "No client connection") + return False + + client.unmute_account(post.account.id) + username = post.account.display_name or post.account.username + self.action_success.emit("unmute", f"Unmuted {username}") + + # Play success sound for successful unmute + if self.sound_manager: + self.sound_manager.play_success() + return True + + except Exception as e: + error_msg = f"Unmute failed: {str(e)}" + self.action_failed.emit("unmute", error_msg) + if self.sound_manager: + self.sound_manager.play_error() + return False \ No newline at end of file diff --git a/src/widgets/timeline_view.py b/src/widgets/timeline_view.py index 55fd2a4..e116c77 100644 --- a/src/widgets/timeline_view.py +++ b/src/widgets/timeline_view.py @@ -30,6 +30,7 @@ from activitypub.client import ActivityPubClient from models.post import Post, Account from models.conversation import Conversation, PleromaChatConversation from widgets.poll_voting_dialog import PollVotingDialog +from managers.post_actions_manager import PostActionsManager class TimelineView(QTreeWidget): @@ -55,6 +56,9 @@ class TimelineView(QTreeWidget): self.logger = logging.getLogger("bifrost.timeline") self.activitypub_client = None self.posts = [] # Store loaded posts + + # Post actions manager for centralized operations + self.post_actions_manager = PostActionsManager(self.account_manager, self.sound_manager) self.oldest_post_id = None # Track for pagination self.newest_post_id = None # Track newest post seen for new content detection self.current_account_id = None # Track which account this newest_post_id belongs to @@ -1730,11 +1734,11 @@ class TimelineView(QTreeWidget): pass def unblock_user_from_list(self, post): - """Unblock a user from the blocked users list""" - try: - # Unblock the user via API - self.activitypub_client.unblock_account(post.account.id) - + """Unblock a user from the blocked users list - USING CENTRALIZED POST ACTIONS MANAGER""" + # Use centralized unblock operation + success = self.post_actions_manager.unblock_user(post) + + if success: # Remove from current timeline display current_item = self.currentItem() if current_item and current_item.data(0, Qt.UserRole) == post: @@ -1745,19 +1749,12 @@ class TimelineView(QTreeWidget): self.takeTopLevelItem(i) break - # Play success sound - self.sound_manager.play_success() - - except Exception as e: - self.logger.error(f"Error unblocking user: {e}") - self.sound_manager.play_error() - def unmute_user_from_list(self, post): - """Unmute a user from the muted users list""" - try: - # Unmute the user via API - self.activitypub_client.unmute_account(post.account.id) - + """Unmute a user from the muted users list - USING CENTRALIZED POST ACTIONS MANAGER""" + # Use centralized unmute operation + success = self.post_actions_manager.unmute_user(post) + + if success: # Remove from current timeline display current_item = self.currentItem() if current_item and current_item.data(0, Qt.UserRole) == post: @@ -1768,13 +1765,6 @@ class TimelineView(QTreeWidget): self.takeTopLevelItem(i) break - # Play success sound - self.sound_manager.play_success() - - except Exception as e: - self.logger.error(f"Error unmuting user: {e}") - self.sound_manager.play_error() - def expand_thread_with_context(self, item): """Expand thread after fetching full conversation context with proper sorting""" try: