Add blocked/muted user management tabs with complete keyboard navigation

- Add Blocked Users and Muted Users timeline tabs with visual indicators (🚫/🔇)
- Implement dedicated context menus for unblocking/unmuting from management tabs
- Add complete keyboard shortcuts for all 10 timeline tabs (Ctrl+1 through Ctrl+0)
- Add block (Ctrl+Shift+B) and mute (Ctrl+Shift+M) keyboard shortcuts with confirmation
- Fix timeline tab indices and add missing shortcuts for all social timelines
- Handle blocked/muted account display with accessibility-friendly status announcements
- Prevent self-blocking/muting with appropriate error messages and sound feedback
- Support immediate removal from lists after unblock/unmute actions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Storm Dragon
2025-07-21 11:15:58 -04:00
parent 684919f4ca
commit 3d9ce28334
2 changed files with 253 additions and 20 deletions

View File

@ -77,6 +77,8 @@ class MainWindow(QMainWindow):
self.timeline_tabs.addTab(QWidget(), "Bookmarks")
self.timeline_tabs.addTab(QWidget(), "Followers")
self.timeline_tabs.addTab(QWidget(), "Following")
self.timeline_tabs.addTab(QWidget(), "Blocked")
self.timeline_tabs.addTab(QWidget(), "Muted")
self.timeline_tabs.currentChanged.connect(self.on_timeline_tab_changed)
main_layout.addWidget(self.timeline_tabs)
@ -174,24 +176,60 @@ class MainWindow(QMainWindow):
home_action.triggered.connect(lambda: self.switch_timeline(0))
timeline_menu.addAction(home_action)
# Messages timeline action
messages_action = QAction("&Messages", self)
messages_action.setShortcut(QKeySequence("Ctrl+2"))
messages_action.triggered.connect(lambda: self.switch_timeline(1))
timeline_menu.addAction(messages_action)
# Mentions timeline action
mentions_action = QAction("&Mentions", self)
mentions_action.setShortcut(QKeySequence("Ctrl+2"))
mentions_action.triggered.connect(lambda: self.switch_timeline(1))
mentions_action = QAction("M&entions", self)
mentions_action.setShortcut(QKeySequence("Ctrl+3"))
mentions_action.triggered.connect(lambda: self.switch_timeline(2))
timeline_menu.addAction(mentions_action)
# Local timeline action
local_action = QAction("&Local", self)
local_action.setShortcut(QKeySequence("Ctrl+3"))
local_action.triggered.connect(lambda: self.switch_timeline(2))
local_action.setShortcut(QKeySequence("Ctrl+4"))
local_action.triggered.connect(lambda: self.switch_timeline(3))
timeline_menu.addAction(local_action)
# Federated timeline action
federated_action = QAction("&Federated", self)
federated_action.setShortcut(QKeySequence("Ctrl+4"))
federated_action.triggered.connect(lambda: self.switch_timeline(3))
federated_action.setShortcut(QKeySequence("Ctrl+5"))
federated_action.triggered.connect(lambda: self.switch_timeline(4))
timeline_menu.addAction(federated_action)
# Bookmarks timeline action
bookmarks_action = QAction("&Bookmarks", self)
bookmarks_action.setShortcut(QKeySequence("Ctrl+6"))
bookmarks_action.triggered.connect(lambda: self.switch_timeline(5))
timeline_menu.addAction(bookmarks_action)
# Followers timeline action
followers_action = QAction("Follo&wers", self)
followers_action.setShortcut(QKeySequence("Ctrl+7"))
followers_action.triggered.connect(lambda: self.switch_timeline(6))
timeline_menu.addAction(followers_action)
# Following timeline action
following_action = QAction("Follo&wing", self)
following_action.setShortcut(QKeySequence("Ctrl+8"))
following_action.triggered.connect(lambda: self.switch_timeline(7))
timeline_menu.addAction(following_action)
# Blocked users timeline action
blocked_action = QAction("Bloc&ked Users", self)
blocked_action.setShortcut(QKeySequence("Ctrl+9"))
blocked_action.triggered.connect(lambda: self.switch_timeline(8))
timeline_menu.addAction(blocked_action)
# Muted users timeline action
muted_action = QAction("M&uted Users", self)
muted_action.setShortcut(QKeySequence("Ctrl+0"))
muted_action.triggered.connect(lambda: self.switch_timeline(9))
timeline_menu.addAction(muted_action)
# Post menu
post_menu = menubar.addMenu("&Post")
@ -258,6 +296,20 @@ class MainWindow(QMainWindow):
social_menu.addSeparator()
# Block action
block_action = QAction("&Block User", self)
block_action.setShortcut(QKeySequence("Ctrl+Shift+B"))
block_action.triggered.connect(self.block_current_user)
social_menu.addAction(block_action)
# Mute action
mute_action = QAction("&Mute User", self)
mute_action.setShortcut(QKeySequence("Ctrl+Shift+M"))
mute_action.triggered.connect(self.mute_current_user)
social_menu.addAction(mute_action)
social_menu.addSeparator()
# Manual follow action
manual_follow_action = QAction("Follow &Specific User...", self)
manual_follow_action.setShortcut(QKeySequence("Ctrl+Shift+M"))
@ -451,8 +503,8 @@ class MainWindow(QMainWindow):
def switch_timeline(self, index, from_tab_change=False):
"""Switch to timeline by index with loading feedback"""
timeline_names = ["Home", "Messages", "Mentions", "Local", "Federated", "Bookmarks", "Followers", "Following"]
timeline_types = ["home", "conversations", "notifications", "local", "federated", "bookmarks", "followers", "following"]
timeline_names = ["Home", "Messages", "Mentions", "Local", "Federated", "Bookmarks", "Followers", "Following", "Blocked", "Muted"]
timeline_types = ["home", "conversations", "notifications", "local", "federated", "bookmarks", "followers", "following", "blocked", "muted"]
if 0 <= index < len(timeline_names):
timeline_name = timeline_names[index]
@ -929,6 +981,91 @@ class MainWindow(QMainWindow):
except Exception as e:
self.status_bar.showMessage(f"Follow failed: {str(e)}", 3000)
def block_current_user(self):
"""Block the user of the currently selected post"""
post = self.get_selected_post()
if post:
self.block_user(post)
else:
self.status_bar.showMessage("No post selected", 2000)
def mute_current_user(self):
"""Mute the user of the currently selected post"""
post = self.get_selected_post()
if post:
self.mute_user(post)
else:
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 = ActivityPubClient(active_account.instance_url, active_account.access_token)
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()
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 = ActivityPubClient(active_account.instance_url, active_account.access_token)
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()
def closeEvent(self, event):
"""Handle window close event"""
# Only play shutdown sound if not already played through quit_application