Add comprehensive user profile viewer with full accessibility support
- Create ProfileDialog with threaded loading and tabbed interface - Display user bio, profile fields, stats, and recent posts - Implement Follow/Unfollow, Block/Unblock, Mute/Unmute actions - Add ActivityPub API methods for social actions and account management - Enable keyboard navigation in read-only bio text with TextInteractionFlags - Replace TODO profile viewing with fully functional dialog - Update documentation to reflect completed profile features 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
32
CLAUDE.md
32
CLAUDE.md
@@ -423,25 +423,27 @@ verbose_announcements = true
|
||||
- **Challenge**: Users want different audio feedback
|
||||
- **Solution**: Comprehensive sound pack system with easy installation
|
||||
|
||||
## Planned Feature Additions (TODO)
|
||||
## Recently Implemented Features
|
||||
|
||||
### High Priority Missing Features
|
||||
- **Direct Message Interface**: Dedicated DM tab with conversation threading (separate from private posts)
|
||||
- **Bookmarks Tab**: Timeline tab for viewing saved/bookmarked posts
|
||||
- **User Blocking**: Block/unblock users with management interface
|
||||
- **User Muting**: Mute/unmute users functionality
|
||||
- **Poll Support**: Create and vote on polls with accessible interface
|
||||
### Completed Features
|
||||
- **Direct Message Interface**: ✅ Dedicated Messages tab with conversation threading
|
||||
- **Bookmarks Tab**: ✅ Timeline tab for viewing saved/bookmarked posts
|
||||
- **Poll Support**: ✅ Create and vote on polls with accessible interface
|
||||
- **Poll Creation**: ✅ Add poll options to compose dialog with expiration times
|
||||
- **Poll Voting**: ✅ Accessible poll interaction with keyboard navigation
|
||||
- **User Profile Viewer**: ✅ Comprehensive profile dialog with bio, fields, recent posts
|
||||
- **Social Actions**: ✅ Follow/unfollow, block/unblock, mute/unmute from profile viewer
|
||||
|
||||
### Medium Priority Features
|
||||
### Remaining High Priority Features
|
||||
- **User Blocking Management**: Block/unblock users with dedicated management interface
|
||||
- **User Muting Management**: Mute/unmute users with management interface
|
||||
- **Blocked Users Management**: Tab/dialog to view and manage blocked users
|
||||
- **Poll Creation**: Add poll options to compose dialog
|
||||
- **Poll Voting**: Accessible poll interaction ("Poll: What's your favorite color? 3 options, press Enter to vote")
|
||||
|
||||
### Implementation Notes
|
||||
- Models already have bookmark, muted, blocking fields - just need API integration
|
||||
- Timeline will need additional tabs: Home, Mentions, Local, Federated, DMs, Bookmarks
|
||||
- Poll accessibility: Announce poll in timeline, Enter to interact, arrow keys to navigate options
|
||||
- DM interface should show conversation threads rather than timeline format
|
||||
### Implementation Status
|
||||
- Timeline tabs completed: Home, Messages, Mentions, Local, Federated, Bookmarks, Followers, Following
|
||||
- Profile viewer includes all social actions (follow, block, mute) with API integration
|
||||
- Poll accessibility fully implemented with screen reader announcements
|
||||
- DM interface shows conversation threads with proper threading
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ This project was created through "vibe coding" - a collaborative development app
|
||||
- **Direct Message Interface**: Dedicated conversation view with threading support
|
||||
- **Bookmarks**: Save and view bookmarked posts in a dedicated timeline
|
||||
- **Poll Support**: Create, vote in, and view results of fediverse polls with full accessibility
|
||||
- **User Profile Viewer**: Comprehensive profile viewing with bio, fields, recent posts, and social actions
|
||||
- **Social Features**: Follow/unfollow, block/unblock, and mute/unmute users directly from profiles
|
||||
|
||||
## Audio System
|
||||
|
||||
|
||||
@@ -385,6 +385,65 @@ class ActivityPubClient:
|
||||
endpoint = f'/api/v1/polls/{poll_id}/votes'
|
||||
data = {'choices': choices}
|
||||
return self._make_request('POST', endpoint, data=data)
|
||||
|
||||
def get_account_statuses(self, account_id: str, limit: int = 40,
|
||||
max_id: Optional[str] = None, since_id: Optional[str] = None,
|
||||
exclude_reblogs: bool = False, exclude_replies: bool = False,
|
||||
only_media: bool = False, pinned: bool = False) -> List[Dict]:
|
||||
"""Get account's statuses/posts"""
|
||||
params = {'limit': limit}
|
||||
if max_id:
|
||||
params['max_id'] = max_id
|
||||
if since_id:
|
||||
params['since_id'] = since_id
|
||||
if exclude_reblogs:
|
||||
params['exclude_reblogs'] = 'true'
|
||||
if exclude_replies:
|
||||
params['exclude_replies'] = 'true'
|
||||
if only_media:
|
||||
params['only_media'] = 'true'
|
||||
if pinned:
|
||||
params['pinned'] = 'true'
|
||||
|
||||
endpoint = f'/api/v1/accounts/{account_id}/statuses'
|
||||
return self._make_request('GET', endpoint, params=params)
|
||||
|
||||
def block_account(self, account_id: str) -> Dict:
|
||||
"""Block an account"""
|
||||
endpoint = f'/api/v1/accounts/{account_id}/block'
|
||||
return self._make_request('POST', endpoint)
|
||||
|
||||
def unblock_account(self, account_id: str) -> Dict:
|
||||
"""Unblock an account"""
|
||||
endpoint = f'/api/v1/accounts/{account_id}/unblock'
|
||||
return self._make_request('POST', endpoint)
|
||||
|
||||
def mute_account(self, account_id: str, notifications: bool = True) -> Dict:
|
||||
"""Mute an account"""
|
||||
endpoint = f'/api/v1/accounts/{account_id}/mute'
|
||||
data = {'notifications': notifications}
|
||||
return self._make_request('POST', endpoint, data=data)
|
||||
|
||||
def unmute_account(self, account_id: str) -> Dict:
|
||||
"""Unmute an account"""
|
||||
endpoint = f'/api/v1/accounts/{account_id}/unmute'
|
||||
return self._make_request('POST', endpoint)
|
||||
|
||||
def get_blocked_accounts(self, limit: int = 40, max_id: Optional[str] = None) -> List[Dict]:
|
||||
"""Get list of blocked accounts"""
|
||||
params = {'limit': limit}
|
||||
if max_id:
|
||||
params['max_id'] = max_id
|
||||
|
||||
return self._make_request('GET', '/api/v1/blocks', params=params)
|
||||
|
||||
def get_muted_accounts(self, limit: int = 40, max_id: Optional[str] = None) -> List[Dict]:
|
||||
"""Get list of muted accounts"""
|
||||
params = {'limit': limit}
|
||||
if max_id:
|
||||
params['max_id'] = max_id
|
||||
|
||||
return self._make_request('GET', '/api/v1/mutes', params=params)
|
||||
|
||||
|
||||
class AuthenticationError(Exception):
|
||||
|
||||
@@ -18,6 +18,7 @@ from widgets.login_dialog import LoginDialog
|
||||
from widgets.account_selector import AccountSelector
|
||||
from widgets.settings_dialog import SettingsDialog
|
||||
from widgets.soundpack_manager_dialog import SoundpackManagerDialog
|
||||
from widgets.profile_dialog import ProfileDialog
|
||||
from activitypub.client import ActivityPubClient
|
||||
|
||||
|
||||
@@ -632,8 +633,49 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def view_profile(self, post):
|
||||
"""View user profile"""
|
||||
# TODO: Implement profile viewing dialog
|
||||
self.status_bar.showMessage(f"Profile viewing not implemented yet: {post.account.display_name}", 3000)
|
||||
try:
|
||||
# Convert Post.account to User-compatible data and open profile dialog
|
||||
from models.user import User
|
||||
|
||||
# Create User object from Account data
|
||||
account = post.account
|
||||
account_data = {
|
||||
'id': account.id,
|
||||
'username': account.username,
|
||||
'acct': account.acct,
|
||||
'display_name': account.display_name,
|
||||
'note': account.note,
|
||||
'url': account.url,
|
||||
'avatar': account.avatar,
|
||||
'avatar_static': account.avatar_static,
|
||||
'header': account.header,
|
||||
'header_static': account.header_static,
|
||||
'locked': account.locked,
|
||||
'bot': account.bot,
|
||||
'discoverable': account.discoverable,
|
||||
'group': account.group,
|
||||
'created_at': account.created_at.isoformat() if account.created_at else None,
|
||||
'followers_count': account.followers_count,
|
||||
'following_count': account.following_count,
|
||||
'statuses_count': account.statuses_count,
|
||||
'fields': [], # Will be loaded from API
|
||||
'emojis': [] # Will be loaded from API
|
||||
}
|
||||
|
||||
user = User.from_api_dict(account_data)
|
||||
|
||||
dialog = ProfileDialog(
|
||||
user_id=user.id,
|
||||
account_manager=self.account_manager,
|
||||
sound_manager=self.timeline.sound_manager,
|
||||
initial_user=user,
|
||||
parent=self
|
||||
)
|
||||
dialog.exec()
|
||||
except Exception as e:
|
||||
self.status_bar.showMessage(f"Error opening profile: {str(e)}", 3000)
|
||||
if hasattr(self.timeline, 'sound_manager'):
|
||||
self.timeline.sound_manager.play_error()
|
||||
|
||||
def update_status_label(self):
|
||||
"""Update the status label with current account info"""
|
||||
|
||||
@@ -234,6 +234,5 @@ class User:
|
||||
'requested': self.requested,
|
||||
'domain_blocking': self.domain_blocking,
|
||||
'showing_reblogs': self.showing_reblogs,
|
||||
'endorsed': self.endorsed,
|
||||
'note_plain': self.note_plain
|
||||
'endorsed': self.endorsed
|
||||
}
|
||||
544
src/widgets/profile_dialog.py
Normal file
544
src/widgets/profile_dialog.py
Normal file
@@ -0,0 +1,544 @@
|
||||
"""
|
||||
User profile dialog for viewing fediverse user profiles
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QTextEdit, QTabWidget, QListWidget, QListWidgetItem,
|
||||
QDialogButtonBox, QScrollArea, QWidget, QFormLayout,
|
||||
QMessageBox, QGroupBox
|
||||
)
|
||||
from PySide6.QtCore import Qt, Signal, QThread, QTimer
|
||||
from PySide6.QtGui import QFont
|
||||
from typing import Optional, List, Dict, Any
|
||||
import html
|
||||
|
||||
from models.user import User, Field
|
||||
from models.post import Post
|
||||
from activitypub.client import ActivityPubClient
|
||||
from config.accounts import AccountManager
|
||||
from audio.sound_manager import SoundManager
|
||||
|
||||
|
||||
class ProfileLoadThread(QThread):
|
||||
"""Thread for loading profile data without blocking UI"""
|
||||
|
||||
profile_loaded = Signal(object) # User object
|
||||
relationship_loaded = Signal(dict) # Relationship dict
|
||||
posts_loaded = Signal(list) # List of recent posts
|
||||
error_occurred = Signal(str) # Error message
|
||||
|
||||
def __init__(self, client: ActivityPubClient, user_id: str):
|
||||
super().__init__()
|
||||
self.client = client
|
||||
self.user_id = user_id
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
# Load profile data
|
||||
profile_data = self.client.get_account(self.user_id)
|
||||
user = User.from_api_dict(profile_data)
|
||||
self.profile_loaded.emit(user)
|
||||
|
||||
# Load relationship
|
||||
relationship_data = self.client.get_relationship(self.user_id)
|
||||
self.relationship_loaded.emit(relationship_data)
|
||||
|
||||
# Load recent posts
|
||||
posts_data = self.client.get_account_statuses(self.user_id, limit=10)
|
||||
posts = [Post.from_api_dict(post_data) for post_data in posts_data]
|
||||
self.posts_loaded.emit(posts)
|
||||
|
||||
except Exception as e:
|
||||
self.error_occurred.emit(str(e))
|
||||
|
||||
|
||||
class ProfileDialog(QDialog):
|
||||
"""Dialog for displaying user profiles with accessibility focus"""
|
||||
|
||||
def __init__(self, user_id: str, account_manager: AccountManager,
|
||||
sound_manager: SoundManager, initial_user: Optional[User] = None,
|
||||
parent=None):
|
||||
super().__init__(parent)
|
||||
self.user_id = user_id
|
||||
self.account_manager = account_manager
|
||||
self.sound_manager = sound_manager
|
||||
self.user = initial_user # May be None, will load from API
|
||||
self.relationship = {}
|
||||
self.recent_posts = []
|
||||
|
||||
# Get API client
|
||||
active_account = self.account_manager.get_active_account()
|
||||
if not active_account:
|
||||
raise ValueError("No active account for profile viewing")
|
||||
|
||||
self.client = ActivityPubClient(
|
||||
active_account.instance_url,
|
||||
active_account.access_token
|
||||
)
|
||||
|
||||
self.setup_ui()
|
||||
self.load_profile_data()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Initialize the user interface"""
|
||||
self.setWindowTitle("User Profile")
|
||||
self.setMinimumSize(600, 500)
|
||||
self.setModal(True)
|
||||
|
||||
# Main layout
|
||||
main_layout = QVBoxLayout(self)
|
||||
|
||||
# Loading label
|
||||
self.loading_label = QLabel("Loading profile...")
|
||||
self.loading_label.setAccessibleName("Profile Loading Status")
|
||||
self.loading_label.setAlignment(Qt.AlignCenter)
|
||||
main_layout.addWidget(self.loading_label)
|
||||
|
||||
# Content widget (initially hidden)
|
||||
self.content_widget = QWidget()
|
||||
self.content_widget.hide()
|
||||
self.setup_content_ui()
|
||||
main_layout.addWidget(self.content_widget)
|
||||
|
||||
# Button box
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
main_layout.addWidget(self.button_box)
|
||||
|
||||
def setup_content_ui(self):
|
||||
"""Setup the main content UI (shown after loading)"""
|
||||
content_layout = QVBoxLayout(self.content_widget)
|
||||
|
||||
# User header section
|
||||
self.setup_header_section(content_layout)
|
||||
|
||||
# Tab widget for different views
|
||||
self.tab_widget = QTabWidget()
|
||||
self.tab_widget.setAccessibleName("Profile Information Tabs")
|
||||
|
||||
# Profile tab
|
||||
self.setup_profile_tab()
|
||||
self.tab_widget.addTab(self.profile_tab, "Profile")
|
||||
|
||||
# Recent posts tab
|
||||
self.setup_posts_tab()
|
||||
self.tab_widget.addTab(self.posts_tab, "Recent Posts")
|
||||
|
||||
content_layout.addWidget(self.tab_widget)
|
||||
|
||||
def setup_header_section(self, parent_layout):
|
||||
"""Setup the profile header with name, stats, and action buttons"""
|
||||
header_widget = QWidget()
|
||||
header_layout = QVBoxLayout(header_widget)
|
||||
|
||||
# User name and handle
|
||||
self.name_label = QLabel()
|
||||
self.name_label.setAccessibleName("User Display Name")
|
||||
name_font = QFont()
|
||||
name_font.setPointSize(14)
|
||||
name_font.setBold(True)
|
||||
self.name_label.setFont(name_font)
|
||||
header_layout.addWidget(self.name_label)
|
||||
|
||||
self.handle_label = QLabel()
|
||||
self.handle_label.setAccessibleName("User Handle")
|
||||
header_layout.addWidget(self.handle_label)
|
||||
|
||||
# Stats row
|
||||
stats_layout = QHBoxLayout()
|
||||
|
||||
self.posts_stat = QLabel()
|
||||
self.posts_stat.setAccessibleName("Post Count")
|
||||
stats_layout.addWidget(self.posts_stat)
|
||||
|
||||
self.following_stat = QLabel()
|
||||
self.following_stat.setAccessibleName("Following Count")
|
||||
stats_layout.addWidget(self.following_stat)
|
||||
|
||||
self.followers_stat = QLabel()
|
||||
self.followers_stat.setAccessibleName("Followers Count")
|
||||
stats_layout.addWidget(self.followers_stat)
|
||||
|
||||
stats_layout.addStretch()
|
||||
header_layout.addLayout(stats_layout)
|
||||
|
||||
# Action buttons
|
||||
actions_layout = QHBoxLayout()
|
||||
|
||||
self.follow_button = QPushButton()
|
||||
self.follow_button.setAccessibleName("Follow or Unfollow User")
|
||||
self.follow_button.clicked.connect(self.toggle_follow)
|
||||
actions_layout.addWidget(self.follow_button)
|
||||
|
||||
self.block_button = QPushButton()
|
||||
self.block_button.setAccessibleName("Block or Unblock User")
|
||||
self.block_button.clicked.connect(self.toggle_block)
|
||||
actions_layout.addWidget(self.block_button)
|
||||
|
||||
self.mute_button = QPushButton()
|
||||
self.mute_button.setAccessibleName("Mute or Unmute User")
|
||||
self.mute_button.clicked.connect(self.toggle_mute)
|
||||
actions_layout.addWidget(self.mute_button)
|
||||
|
||||
actions_layout.addStretch()
|
||||
header_layout.addLayout(actions_layout)
|
||||
|
||||
parent_layout.addWidget(header_widget)
|
||||
|
||||
def setup_profile_tab(self):
|
||||
"""Setup the profile information tab"""
|
||||
self.profile_tab = QScrollArea()
|
||||
self.profile_tab.setAccessibleName("Profile Information")
|
||||
self.profile_tab.setWidgetResizable(True)
|
||||
|
||||
profile_content = QWidget()
|
||||
profile_layout = QVBoxLayout(profile_content)
|
||||
|
||||
# Bio section
|
||||
bio_group = QGroupBox("Biography")
|
||||
bio_group.setAccessibleName("User Biography Section")
|
||||
bio_layout = QVBoxLayout(bio_group)
|
||||
|
||||
self.bio_text = QTextEdit()
|
||||
self.bio_text.setAccessibleName("User Biography")
|
||||
self.bio_text.setReadOnly(True)
|
||||
self.bio_text.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard | Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
|
||||
self.bio_text.setMaximumHeight(100)
|
||||
bio_layout.addWidget(self.bio_text)
|
||||
|
||||
profile_layout.addWidget(bio_group)
|
||||
|
||||
# Profile fields section
|
||||
self.fields_group = QGroupBox("Profile Fields")
|
||||
self.fields_group.setAccessibleName("Profile Fields Section")
|
||||
self.fields_layout = QFormLayout(self.fields_group)
|
||||
profile_layout.addWidget(self.fields_group)
|
||||
|
||||
# Account info section
|
||||
info_group = QGroupBox("Account Information")
|
||||
info_group.setAccessibleName("Account Information Section")
|
||||
info_layout = QFormLayout(info_group)
|
||||
|
||||
self.joined_label = QLabel()
|
||||
self.joined_label.setAccessibleName("Account Creation Date")
|
||||
info_layout.addRow("Joined:", self.joined_label)
|
||||
|
||||
self.last_active_label = QLabel()
|
||||
self.last_active_label.setAccessibleName("Last Activity Date")
|
||||
info_layout.addRow("Last Active:", self.last_active_label)
|
||||
|
||||
self.account_type_label = QLabel()
|
||||
self.account_type_label.setAccessibleName("Account Type")
|
||||
info_layout.addRow("Account Type:", self.account_type_label)
|
||||
|
||||
profile_layout.addWidget(info_group)
|
||||
|
||||
profile_layout.addStretch()
|
||||
self.profile_tab.setWidget(profile_content)
|
||||
|
||||
def setup_posts_tab(self):
|
||||
"""Setup the recent posts tab"""
|
||||
self.posts_tab = QWidget()
|
||||
posts_layout = QVBoxLayout(self.posts_tab)
|
||||
|
||||
posts_label = QLabel("Recent Posts")
|
||||
posts_label.setAccessibleName("Recent Posts Section")
|
||||
font = QFont()
|
||||
font.setBold(True)
|
||||
posts_label.setFont(font)
|
||||
posts_layout.addWidget(posts_label)
|
||||
|
||||
self.posts_list = QListWidget()
|
||||
self.posts_list.setAccessibleName("Recent Posts List")
|
||||
posts_layout.addWidget(self.posts_list)
|
||||
|
||||
def load_profile_data(self):
|
||||
"""Load profile data in background thread"""
|
||||
if self.user:
|
||||
# We already have basic user data, just load relationship and posts
|
||||
self.display_user_data()
|
||||
|
||||
# Always load fresh data from API
|
||||
self.load_thread = ProfileLoadThread(self.client, self.user_id)
|
||||
self.load_thread.profile_loaded.connect(self.on_profile_loaded)
|
||||
self.load_thread.relationship_loaded.connect(self.on_relationship_loaded)
|
||||
self.load_thread.posts_loaded.connect(self.on_posts_loaded)
|
||||
self.load_thread.error_occurred.connect(self.on_load_error)
|
||||
self.load_thread.start()
|
||||
|
||||
def on_profile_loaded(self, user: User):
|
||||
"""Handle profile data being loaded"""
|
||||
self.user = user
|
||||
self.display_user_data()
|
||||
|
||||
def on_relationship_loaded(self, relationship_data: dict):
|
||||
"""Handle relationship data being loaded"""
|
||||
self.relationship = relationship_data
|
||||
self.update_action_buttons()
|
||||
|
||||
def on_posts_loaded(self, posts: List[Post]):
|
||||
"""Handle recent posts being loaded"""
|
||||
self.recent_posts = posts
|
||||
self.display_recent_posts()
|
||||
|
||||
# Hide loading label and show content
|
||||
self.loading_label.hide()
|
||||
self.content_widget.show()
|
||||
|
||||
# Play success sound
|
||||
self.sound_manager.play_success()
|
||||
|
||||
def on_load_error(self, error_message: str):
|
||||
"""Handle loading error"""
|
||||
self.loading_label.setText(f"Error loading profile: {error_message}")
|
||||
self.sound_manager.play_error()
|
||||
|
||||
def display_user_data(self):
|
||||
"""Display the loaded user data"""
|
||||
if not self.user:
|
||||
return
|
||||
|
||||
# Header information
|
||||
display_name = self.user.get_display_name()
|
||||
self.name_label.setText(display_name)
|
||||
|
||||
full_username = f"@{self.user.get_full_username()}"
|
||||
self.handle_label.setText(full_username)
|
||||
|
||||
# Stats
|
||||
self.posts_stat.setText(f"{self.user.statuses_count:,} posts")
|
||||
self.following_stat.setText(f"{self.user.following_count:,} following")
|
||||
self.followers_stat.setText(f"{self.user.followers_count:,} followers")
|
||||
|
||||
# Biography
|
||||
bio_html = self.user.note if self.user.note else "No biography provided."
|
||||
self.bio_text.setHtml(bio_html)
|
||||
|
||||
# Profile fields
|
||||
self.display_profile_fields()
|
||||
|
||||
# Account information
|
||||
if self.user.created_at:
|
||||
joined_text = self.user.created_at.strftime("%B %d, %Y")
|
||||
self.joined_label.setText(joined_text)
|
||||
else:
|
||||
self.joined_label.setText("Unknown")
|
||||
|
||||
if self.user.last_status_at:
|
||||
last_active_text = self.user.last_status_at.strftime("%B %d, %Y")
|
||||
self.last_active_label.setText(last_active_text)
|
||||
else:
|
||||
self.last_active_label.setText("Unknown")
|
||||
|
||||
# Account type
|
||||
account_type_parts = []
|
||||
if self.user.bot:
|
||||
account_type_parts.append("Bot")
|
||||
if self.user.group:
|
||||
account_type_parts.append("Group")
|
||||
if self.user.locked:
|
||||
account_type_parts.append("Private")
|
||||
|
||||
if account_type_parts:
|
||||
account_type = ", ".join(account_type_parts)
|
||||
else:
|
||||
account_type = "Public User"
|
||||
|
||||
self.account_type_label.setText(account_type)
|
||||
|
||||
# Update window title
|
||||
self.setWindowTitle(f"Profile: {display_name}")
|
||||
|
||||
def display_profile_fields(self):
|
||||
"""Display user profile fields"""
|
||||
# Clear existing fields
|
||||
while self.fields_layout.rowCount() > 0:
|
||||
self.fields_layout.removeRow(0)
|
||||
|
||||
if not self.user.fields:
|
||||
no_fields_label = QLabel("No profile fields")
|
||||
no_fields_label.setAccessibleName("No Profile Fields Message")
|
||||
self.fields_layout.addRow(no_fields_label)
|
||||
return
|
||||
|
||||
for field in self.user.fields:
|
||||
field_name = html.unescape(field.name)
|
||||
field_value = html.unescape(field.value)
|
||||
|
||||
# Create value label
|
||||
value_label = QLabel(field_value)
|
||||
value_label.setAccessibleName(f"Profile Field: {field_name}")
|
||||
value_label.setWordWrap(True)
|
||||
value_label.setTextFormat(Qt.RichText)
|
||||
value_label.setOpenExternalLinks(True)
|
||||
|
||||
# Add verification indicator if field is verified
|
||||
if field.verified_at:
|
||||
field_name += " ✓"
|
||||
value_label.setStyleSheet("QLabel { color: green; }")
|
||||
|
||||
self.fields_layout.addRow(f"{field_name}:", value_label)
|
||||
|
||||
def display_recent_posts(self):
|
||||
"""Display recent posts"""
|
||||
self.posts_list.clear()
|
||||
|
||||
if not self.recent_posts:
|
||||
no_posts_item = QListWidgetItem("No recent posts")
|
||||
no_posts_item.setData(Qt.AccessibleTextRole, "No recent posts available")
|
||||
self.posts_list.addItem(no_posts_item)
|
||||
return
|
||||
|
||||
for post in self.recent_posts:
|
||||
# Create post summary for list
|
||||
content_text = post.get_content_text()
|
||||
if len(content_text) > 100:
|
||||
content_text = content_text[:100] + "..."
|
||||
|
||||
created_date = post.created_at.strftime("%m/%d %H:%M") if post.created_at else "Unknown"
|
||||
|
||||
item_text = f"[{created_date}] {content_text}"
|
||||
|
||||
item = QListWidgetItem(item_text)
|
||||
item.setData(Qt.UserRole, post)
|
||||
|
||||
# Accessibility description
|
||||
accessibility_text = f"Post from {created_date}: {post.get_content_text()}"
|
||||
item.setData(Qt.AccessibleTextRole, accessibility_text)
|
||||
|
||||
self.posts_list.addItem(item)
|
||||
|
||||
def update_action_buttons(self):
|
||||
"""Update action buttons based on relationship status"""
|
||||
if not self.relationship:
|
||||
# No relationship data yet, disable buttons
|
||||
self.follow_button.setEnabled(False)
|
||||
self.block_button.setEnabled(False)
|
||||
self.mute_button.setEnabled(False)
|
||||
return
|
||||
|
||||
# Follow/Unfollow button
|
||||
if self.relationship.get('following', False):
|
||||
self.follow_button.setText("Unfollow")
|
||||
self.follow_button.setAccessibleName("Unfollow this user")
|
||||
elif self.relationship.get('requested', False):
|
||||
self.follow_button.setText("Cancel Request")
|
||||
self.follow_button.setAccessibleName("Cancel follow request")
|
||||
else:
|
||||
self.follow_button.setText("Follow")
|
||||
self.follow_button.setAccessibleName("Follow this user")
|
||||
|
||||
self.follow_button.setEnabled(True)
|
||||
|
||||
# Block/Unblock button
|
||||
if self.relationship.get('blocking', False):
|
||||
self.block_button.setText("Unblock")
|
||||
self.block_button.setAccessibleName("Unblock this user")
|
||||
else:
|
||||
self.block_button.setText("Block")
|
||||
self.block_button.setAccessibleName("Block this user")
|
||||
|
||||
self.block_button.setEnabled(True)
|
||||
|
||||
# Mute/Unmute button
|
||||
if self.relationship.get('muting', False):
|
||||
self.mute_button.setText("Unmute")
|
||||
self.mute_button.setAccessibleName("Unmute this user")
|
||||
else:
|
||||
self.mute_button.setText("Mute")
|
||||
self.mute_button.setAccessibleName("Mute this user")
|
||||
|
||||
self.mute_button.setEnabled(True)
|
||||
|
||||
def toggle_follow(self):
|
||||
"""Toggle follow status for this user"""
|
||||
if not self.relationship:
|
||||
return
|
||||
|
||||
try:
|
||||
if self.relationship.get('following', False):
|
||||
# Unfollow
|
||||
self.client.unfollow_account(self.user_id)
|
||||
self.relationship['following'] = False
|
||||
self.relationship['requested'] = False
|
||||
self.sound_manager.play_unfollow()
|
||||
|
||||
elif self.relationship.get('requested', False):
|
||||
# Cancel follow request
|
||||
self.client.unfollow_account(self.user_id)
|
||||
self.relationship['requested'] = False
|
||||
self.sound_manager.play_success()
|
||||
|
||||
else:
|
||||
# Follow
|
||||
result = self.client.follow_account(self.user_id)
|
||||
if result.get('following', False):
|
||||
self.relationship['following'] = True
|
||||
else:
|
||||
self.relationship['requested'] = True
|
||||
self.sound_manager.play_follow()
|
||||
|
||||
self.update_action_buttons()
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Follow Error", f"Failed to update follow status: {e}")
|
||||
self.sound_manager.play_error()
|
||||
|
||||
def toggle_block(self):
|
||||
"""Toggle block status for this user"""
|
||||
if not self.relationship:
|
||||
return
|
||||
|
||||
try:
|
||||
if self.relationship.get('blocking', False):
|
||||
# Unblock
|
||||
self.client.unblock_account(self.user_id)
|
||||
self.relationship['blocking'] = False
|
||||
self.sound_manager.play_success()
|
||||
else:
|
||||
# Block
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
"Block User",
|
||||
f"Are you sure you want to block @{self.user.get_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:
|
||||
self.client.block_account(self.user_id)
|
||||
self.relationship['blocking'] = True
|
||||
self.relationship['following'] = False
|
||||
self.relationship['followed_by'] = False
|
||||
self.sound_manager.play_success()
|
||||
|
||||
self.update_action_buttons()
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Block Error", f"Failed to update block status: {e}")
|
||||
self.sound_manager.play_error()
|
||||
|
||||
def toggle_mute(self):
|
||||
"""Toggle mute status for this user"""
|
||||
if not self.relationship:
|
||||
return
|
||||
|
||||
try:
|
||||
if self.relationship.get('muting', False):
|
||||
# Unmute
|
||||
self.client.unmute_account(self.user_id)
|
||||
self.relationship['muting'] = False
|
||||
self.sound_manager.play_success()
|
||||
else:
|
||||
# Mute
|
||||
self.client.mute_account(self.user_id)
|
||||
self.relationship['muting'] = True
|
||||
self.sound_manager.play_success()
|
||||
|
||||
self.update_action_buttons()
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Mute Error", f"Failed to update mute status: {e}")
|
||||
self.sound_manager.play_error()
|
||||
Reference in New Issue
Block a user