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:
@ -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
|
||||
|
Reference in New Issue
Block a user