Add comprehensive profile editing functionality with accessibility features

- Added update_credentials method to ActivityPub client for profile API calls
- Created ProfileEditDialog with tabbed interface for editing profile information
- Implemented profile fields management with accessible list navigation
- Added HTML stripping for clean display of profile field content
- Integrated profile editing into File menu with Ctrl+Alt+E shortcut
- Fixed Tab key accessibility in biography text field
- Updated documentation with new feature and keyboard shortcut

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Storm Dragon
2025-08-17 08:38:28 -04:00
parent aa65a07345
commit 1bbbb235e8
5 changed files with 669 additions and 0 deletions

View File

@@ -433,6 +433,7 @@ Due to Qt's visual display synchronization, thread collapse may require double-o
- **Ctrl+S**: Open Search dialog for users, hashtags, and posts
- **Ctrl+Shift+F**: Open Timeline Filters dialog
- **Ctrl+Shift+E**: Edit selected post (your own posts only)
- **Ctrl+Alt+E**: Edit your profile (display name, bio, fields, privacy settings)
- **Shift+Delete**: Delete selected post (your own posts only)
- **F5**: Refresh timeline
- **Ctrl+,**: Settings
@@ -560,6 +561,7 @@ verbose_announcements = true
- **Timeline Filtering**: ✅ Filter home timeline by content type (replies, boosts, mentions) and media
- **Search Dialog**: ✅ Dedicated search interface with tabbed results and background processing
- **Filter Dialog**: ✅ Timeline filter configuration with real-time application
- **Profile Editing**: ✅ Edit your profile information including display name, bio, profile fields, and privacy settings
### Remaining High Priority Features
- **User Blocking Management**: Block/unblock users with dedicated management interface

View File

@@ -33,6 +33,7 @@ This project was created through "vibe coding" - a collaborative development app
- **Blocked/Muted Management**: Dedicated tabs for managing blocked and muted users
- **Custom Emoji Support**: Instance-specific emoji support with caching
- **Post Editing**: Edit your own posts and direct messages with full content preservation
- **Profile Editing**: Edit your profile information including display name, bio, profile fields, and privacy settings
- **Search Functionality**: Search for users, hashtags, and posts across the fediverse with dedicated search interface
- **Timeline Filtering**: Customize your timeline view by hiding/showing replies, boosts, mentions, and media content
@@ -128,6 +129,7 @@ Bifrost includes a sophisticated sound system with intelligent notification hand
- **Ctrl+Shift+F**: Open Timeline Filters dialog
- **Ctrl+,**: Open Settings
- **Ctrl+Shift+A**: Add new account
- **Ctrl+Alt+E**: Edit your profile
- **Ctrl+Alt+S**: Open Soundpack Manager
- **Ctrl+Q**: Quit application

View File

@@ -245,6 +245,38 @@ class ActivityPubClient:
"""Verify account credentials"""
return self._make_request('GET', '/api/v1/accounts/verify_credentials')
def update_credentials(self, display_name: Optional[str] = None, note: Optional[str] = None,
avatar: Optional[str] = None, header: Optional[str] = None,
locked: Optional[bool] = None, bot: Optional[bool] = None,
discoverable: Optional[bool] = None, fields_attributes: Optional[List[Dict]] = None) -> Dict:
"""Update account credentials and profile information"""
data = {}
if display_name is not None:
data['display_name'] = display_name
if note is not None:
data['note'] = note
if avatar is not None:
data['avatar'] = avatar
if header is not None:
data['header'] = header
if locked is not None:
data['locked'] = str(locked).lower()
if bot is not None:
data['bot'] = str(bot).lower()
if discoverable is not None:
data['discoverable'] = str(discoverable).lower()
# Handle profile fields
if fields_attributes is not None:
for i, field in enumerate(fields_attributes):
if 'name' in field:
data[f'fields_attributes[{i}][name]'] = field['name']
if 'value' in field:
data[f'fields_attributes[{i}][value]'] = field['value']
return self._make_request_form('PATCH', '/api/v1/accounts/update_credentials', data=data)
def get_timeline(self, timeline_type: str = 'home', limit: int = 40,
max_id: Optional[str] = None, since_id: Optional[str] = None) -> List[Dict]:
"""Get timeline posts"""

View File

@@ -27,6 +27,7 @@ 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 widgets.profile_edit_dialog import ProfileEditDialog
from widgets.accessible_text_dialog import AccessibleTextDialog
from widgets.search_dialog import SearchDialog
from widgets.timeline_filter_dialog import TimelineFilterDialog
@@ -182,6 +183,12 @@ class MainWindow(QMainWindow):
add_account_action.triggered.connect(self.show_login_dialog)
file_menu.addAction(add_account_action)
# Edit Profile action
edit_profile_action = QAction("&Edit Profile", self)
edit_profile_action.setShortcut(QKeySequence("Ctrl+Alt+E"))
edit_profile_action.triggered.connect(self.show_edit_profile)
file_menu.addAction(edit_profile_action)
file_menu.addSeparator()
# Settings action
@@ -809,6 +816,58 @@ class MainWindow(QMainWindow):
self.status_bar.showMessage("Settings saved successfully", 2000)
def show_edit_profile(self):
"""Show the profile editing dialog"""
try:
# Check if we have an active account
if not self.account_manager.has_accounts():
AccessibleTextDialog.show_error(
"No Account",
"You need to be logged in to edit your profile.",
"",
self
)
return
active_account = self.account_manager.get_active_account()
if not active_account:
AccessibleTextDialog.show_error(
"No Active Account",
"Please select an active account to edit your profile.",
"",
self
)
return
# Create and show dialog
dialog = ProfileEditDialog(
self.account_manager,
self.timeline.sound_manager if hasattr(self.timeline, 'sound_manager') else None,
self
)
dialog.profile_updated.connect(self.on_profile_updated)
dialog.exec()
except Exception as e:
self.logger.error(f"Failed to show profile edit dialog: {e}")
AccessibleTextDialog.show_error(
"Profile Edit Error",
"Failed to open profile editor.",
str(e),
self
)
if hasattr(self.timeline, 'sound_manager'):
self.timeline.sound_manager.play_error()
def on_profile_updated(self, updated_user):
"""Handle successful profile update"""
self.logger.info(f"Profile updated for user: {updated_user.username}")
self.status_bar.showMessage("Profile updated successfully", 3000)
# Refresh timeline to show any changes (display name, etc.)
if hasattr(self.timeline, 'refresh_timeline'):
self.timeline.refresh_timeline()
def show_soundpack_manager(self):
"""Show the soundpack manager dialog"""
dialog = SoundpackManagerDialog(self.settings, self)

View File

@@ -0,0 +1,574 @@
"""
Profile editing dialog for Bifrost
"""
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
QLabel, QPushButton, QDialogButtonBox, QTextEdit,
QLineEdit, QCheckBox, QGroupBox, QTabWidget, QWidget,
QScrollArea, QMessageBox, QListWidget, QListWidgetItem
)
from PySide6.QtCore import Qt, Signal, QThread
from PySide6.QtGui import QFont
from typing import Optional, List, Dict, Any
import logging
import re
from models.user import User, Field
from activitypub.client import ActivityPubClient
from config.accounts import AccountManager
from audio.sound_manager import SoundManager
from widgets.accessible_text_dialog import AccessibleTextDialog
def strip_html_tags(text: str) -> str:
"""Strip HTML tags and decode entities for clean text display"""
if not text:
return ""
# Remove HTML tags
clean_text = re.sub(r'<[^>]+>', '', text)
# Decode common HTML entities
clean_text = clean_text.replace('&lt;', '<').replace('&gt;', '>').replace('&amp;', '&')
clean_text = clean_text.replace('&quot;', '"').replace('&#39;', "'")
return clean_text.strip()
class ProfileUpdateThread(QThread):
"""Thread for updating profile data without blocking UI"""
profile_updated = Signal(object) # Updated User object
error_occurred = Signal(str) # Error message
def __init__(self, client: ActivityPubClient, profile_data: Dict[str, Any]):
super().__init__()
self.client = client
self.profile_data = profile_data
self.logger = logging.getLogger('bifrost.profile_edit')
def run(self):
try:
self.logger.info(f"Updating profile with data: {list(self.profile_data.keys())}")
# Update profile via API
result = self.client.update_credentials(**self.profile_data)
# Convert response to User object
updated_user = User.from_api_dict(result)
self.profile_updated.emit(updated_user)
except Exception as e:
self.logger.error(f"Profile update failed: {e}")
self.error_occurred.emit(str(e))
class ProfileFieldsWidget(QWidget):
"""Widget for managing profile fields"""
def __init__(self, parent=None):
super().__init__(parent)
self.fields = [] # List of field dictionaries
self.setup_ui()
def setup_ui(self):
"""Setup the profile fields UI"""
layout = QVBoxLayout(self)
# Instructions
instructions = QLabel("Profile fields are name-value pairs displayed on your profile. You can have up to 4 fields.")
instructions.setAccessibleName("Profile Fields Instructions")
instructions.setWordWrap(True)
layout.addWidget(instructions)
# Fields list
self.fields_list = QListWidget()
self.fields_list.setAccessibleName("Profile Fields List")
layout.addWidget(self.fields_list)
# Buttons
buttons_layout = QHBoxLayout()
self.add_button = QPushButton("Add Field")
self.add_button.setAccessibleName("Add Profile Field")
self.add_button.clicked.connect(self.add_field)
buttons_layout.addWidget(self.add_button)
self.edit_button = QPushButton("Edit Field")
self.edit_button.setAccessibleName("Edit Selected Profile Field")
self.edit_button.clicked.connect(self.edit_field)
self.edit_button.setEnabled(False)
buttons_layout.addWidget(self.edit_button)
self.remove_button = QPushButton("Remove Field")
self.remove_button.setAccessibleName("Remove Selected Profile Field")
self.remove_button.clicked.connect(self.remove_field)
self.remove_button.setEnabled(False)
buttons_layout.addWidget(self.remove_button)
buttons_layout.addStretch()
layout.addLayout(buttons_layout)
# Connect list selection
self.fields_list.itemSelectionChanged.connect(self.on_selection_changed)
def set_fields(self, fields: List[Field]):
"""Set the profile fields"""
self.fields = []
for field in fields:
# Store the original HTML values (they'll be cleaned for display)
self.fields.append({
'name': field.name,
'value': field.value
})
self.refresh_list()
def get_fields(self) -> List[Dict[str, str]]:
"""Get the current profile fields as API format"""
return self.fields.copy()
def refresh_list(self):
"""Refresh the fields list display"""
self.fields_list.clear()
# Add fake header for single-item navigation (accessibility requirement)
field_count = len(self.fields)
header_text = f"Profile fields ({field_count} configured):"
fake_header = QListWidgetItem(header_text)
fake_header.setFlags(Qt.ItemIsEnabled) # Not selectable
fake_header.setData(Qt.AccessibleDescriptionRole, "Profile fields list header")
self.fields_list.addItem(fake_header)
if not self.fields:
item = QListWidgetItem("No profile fields configured")
item.setData(Qt.UserRole, None)
item.setData(Qt.AccessibleTextRole, "No profile fields have been configured")
self.fields_list.addItem(item)
return
for i, field in enumerate(self.fields):
# Strip HTML for clean display in list
clean_name = strip_html_tags(field['name'])
clean_value = strip_html_tags(field['value'])
display_text = f"{clean_name}: {clean_value}"
item = QListWidgetItem(display_text)
item.setData(Qt.UserRole, i)
item.setData(Qt.AccessibleTextRole, f"Profile field {i+1}: {clean_name} equals {clean_value}")
self.fields_list.addItem(item)
def on_selection_changed(self):
"""Handle field selection changes"""
current_item = self.fields_list.currentItem()
# Enable buttons only if it's not the header item and has field data
has_field_data = current_item and current_item.data(Qt.UserRole) is not None
self.edit_button.setEnabled(has_field_data)
self.remove_button.setEnabled(has_field_data)
def add_field(self):
"""Add a new profile field"""
if len(self.fields) >= 4:
QMessageBox.warning(self, "Maximum Fields", "You can only have up to 4 profile fields.")
return
field_data = self.show_field_dialog()
if field_data:
self.fields.append(field_data)
self.refresh_list()
def edit_field(self):
"""Edit the selected profile field"""
current_item = self.fields_list.currentItem()
if not current_item:
return
field_index = current_item.data(Qt.UserRole)
if field_index is None:
return
current_field = self.fields[field_index]
field_data = self.show_field_dialog(current_field)
if field_data:
self.fields[field_index] = field_data
self.refresh_list()
def remove_field(self):
"""Remove the selected profile field"""
current_item = self.fields_list.currentItem()
if not current_item:
return
field_index = current_item.data(Qt.UserRole)
if field_index is None:
return
result = QMessageBox.question(
self,
"Remove Field",
f"Are you sure you want to remove the field '{self.fields[field_index]['name']}'?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if result == QMessageBox.Yes:
del self.fields[field_index]
self.refresh_list()
def show_field_dialog(self, existing_field: Optional[Dict[str, str]] = None) -> Optional[Dict[str, str]]:
"""Show dialog for editing a field"""
dialog = QDialog(self)
dialog.setWindowTitle("Edit Profile Field" if existing_field else "Add Profile Field")
dialog.setModal(True)
dialog.setMinimumSize(400, 200)
layout = QVBoxLayout(dialog)
form_layout = QFormLayout()
# Field name
name_edit = QLineEdit()
name_edit.setAccessibleName("Field Name")
name_edit.setPlaceholderText("e.g., Website, Location, Pronouns")
if existing_field:
# Strip HTML for clean editing
clean_name = strip_html_tags(existing_field['name'])
name_edit.setText(clean_name)
form_layout.addRow("Field Name:", name_edit)
# Field value
value_edit = QLineEdit()
value_edit.setAccessibleName("Field Value")
value_edit.setPlaceholderText("e.g., https://example.com, New York, they/them")
if existing_field:
# Strip HTML for clean editing
clean_value = strip_html_tags(existing_field['value'])
value_edit.setText(clean_value)
form_layout.addRow("Field Value:", value_edit)
layout.addLayout(form_layout)
# Buttons
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(dialog.accept)
button_box.rejected.connect(dialog.reject)
layout.addWidget(button_box)
# Focus on name field
name_edit.setFocus()
if dialog.exec() == QDialog.Accepted:
name = name_edit.text().strip()
value = value_edit.text().strip()
if not name:
QMessageBox.warning(dialog, "Invalid Field", "Field name cannot be empty.")
return None
return {'name': name, 'value': value}
return None
class ProfileEditDialog(QDialog):
"""Dialog for editing user profile information"""
profile_updated = Signal(object) # Emitted when profile is successfully updated
def __init__(self, account_manager: AccountManager, sound_manager: SoundManager, parent=None):
super().__init__(parent)
self.account_manager = account_manager
self.sound_manager = sound_manager
self.current_user = None
self.logger = logging.getLogger('bifrost.profile_edit')
# Get API client
active_account = self.account_manager.get_active_account()
if not active_account:
raise ValueError("No active account for profile editing")
self.client = ActivityPubClient(
active_account.instance_url,
active_account.access_token
)
self.setup_ui()
self.load_current_profile()
def setup_ui(self):
"""Initialize the user interface"""
self.setWindowTitle("Edit Profile")
self.setMinimumSize(600, 500)
self.setModal(True)
# Main layout
main_layout = QVBoxLayout(self)
# Loading label
self.loading_label = QLabel("Loading your 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.Save | QDialogButtonBox.Cancel)
self.button_box.button(QDialogButtonBox.Save).setText("Save Profile")
self.button_box.button(QDialogButtonBox.Save).setAccessibleName("Save Profile Changes")
self.button_box.accepted.connect(self.save_profile)
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)
# Tab widget for different sections
self.tab_widget = QTabWidget()
self.tab_widget.setAccessibleName("Profile Edit Sections")
# Basic info tab
self.setup_basic_tab()
self.tab_widget.addTab(self.basic_tab, "Basic Info")
# Profile fields tab
self.setup_fields_tab()
self.tab_widget.addTab(self.fields_tab, "Profile Fields")
# Privacy settings tab
self.setup_privacy_tab()
self.tab_widget.addTab(self.privacy_tab, "Privacy")
content_layout.addWidget(self.tab_widget)
def setup_basic_tab(self):
"""Setup the basic profile information tab"""
self.basic_tab = QScrollArea()
self.basic_tab.setAccessibleName("Basic Profile Information")
self.basic_tab.setWidgetResizable(True)
basic_content = QWidget()
basic_layout = QVBoxLayout(basic_content)
# Display name
name_group = QGroupBox("Display Name")
name_group.setAccessibleName("Display Name Section")
name_layout = QVBoxLayout(name_group)
self.display_name_edit = QLineEdit()
self.display_name_edit.setAccessibleName("Display Name")
self.display_name_edit.setPlaceholderText("Your display name (shown on posts)")
name_layout.addWidget(self.display_name_edit)
basic_layout.addWidget(name_group)
# Biography
bio_group = QGroupBox("Biography")
bio_group.setAccessibleName("Biography Section")
bio_layout = QVBoxLayout(bio_group)
bio_instructions = QLabel("Describe yourself in a few sentences. HTML and links are supported.")
bio_instructions.setAccessibleName("Biography Instructions")
bio_instructions.setWordWrap(True)
bio_layout.addWidget(bio_instructions)
self.bio_edit = QTextEdit()
self.bio_edit.setAccessibleName("Biography")
self.bio_edit.setPlaceholderText("Tell people about yourself...")
self.bio_edit.setMaximumHeight(150)
# Fix accessibility: Tab should move focus, not insert tab character
self.bio_edit.setTabChangesFocus(True)
bio_layout.addWidget(self.bio_edit)
basic_layout.addWidget(bio_group)
basic_layout.addStretch()
self.basic_tab.setWidget(basic_content)
def setup_fields_tab(self):
"""Setup the profile fields tab"""
self.fields_tab = QWidget()
fields_layout = QVBoxLayout(self.fields_tab)
self.fields_widget = ProfileFieldsWidget()
fields_layout.addWidget(self.fields_widget)
def setup_privacy_tab(self):
"""Setup the privacy settings tab"""
self.privacy_tab = QScrollArea()
self.privacy_tab.setAccessibleName("Privacy Settings")
self.privacy_tab.setWidgetResizable(True)
privacy_content = QWidget()
privacy_layout = QVBoxLayout(privacy_content)
# Account privacy
privacy_group = QGroupBox("Account Privacy")
privacy_group.setAccessibleName("Account Privacy Section")
privacy_group_layout = QVBoxLayout(privacy_group)
self.locked_checkbox = QCheckBox("Private account (require approval for followers)")
self.locked_checkbox.setAccessibleName("Private Account Setting")
privacy_group_layout.addWidget(self.locked_checkbox)
privacy_layout.addWidget(privacy_group)
# Account type
type_group = QGroupBox("Account Type")
type_group.setAccessibleName("Account Type Section")
type_group_layout = QVBoxLayout(type_group)
self.bot_checkbox = QCheckBox("This is a bot account")
self.bot_checkbox.setAccessibleName("Bot Account Setting")
type_group_layout.addWidget(self.bot_checkbox)
privacy_layout.addWidget(type_group)
# Discoverability
discovery_group = QGroupBox("Discoverability")
discovery_group.setAccessibleName("Discoverability Section")
discovery_group_layout = QVBoxLayout(discovery_group)
self.discoverable_checkbox = QCheckBox("Include in directory and search results")
self.discoverable_checkbox.setAccessibleName("Discoverable Account Setting")
discovery_group_layout.addWidget(self.discoverable_checkbox)
privacy_layout.addWidget(discovery_group)
privacy_layout.addStretch()
self.privacy_tab.setWidget(privacy_content)
def load_current_profile(self):
"""Load the current user's profile information"""
try:
# Get current user info
user_data = self.client.verify_credentials()
self.current_user = User.from_api_dict(user_data)
self.populate_fields()
# Hide loading and show content
self.loading_label.hide()
self.content_widget.show()
self.logger.info("Profile loaded successfully for editing")
self.sound_manager.play_success()
except Exception as e:
self.logger.error(f"Failed to load profile for editing: {e}")
self.loading_label.setText(f"Error loading profile: {str(e)}")
self.sound_manager.play_error()
def populate_fields(self):
"""Populate the form fields with current profile data"""
if not self.current_user:
return
# Basic info
self.display_name_edit.setText(self.current_user.display_name or "")
self.bio_edit.setPlainText(self.current_user.note or "")
# Profile fields
self.fields_widget.set_fields(self.current_user.fields or [])
# Privacy settings
self.locked_checkbox.setChecked(self.current_user.locked or False)
self.bot_checkbox.setChecked(self.current_user.bot or False)
self.discoverable_checkbox.setChecked(getattr(self.current_user, 'discoverable', True))
def save_profile(self):
"""Save the profile changes"""
try:
# Collect changes
profile_data = {}
# Check what changed
new_display_name = self.display_name_edit.text().strip()
if new_display_name != (self.current_user.display_name or ""):
profile_data['display_name'] = new_display_name
new_bio = self.bio_edit.toPlainText().strip()
if new_bio != (self.current_user.note or ""):
profile_data['note'] = new_bio
new_locked = self.locked_checkbox.isChecked()
if new_locked != (self.current_user.locked or False):
profile_data['locked'] = new_locked
new_bot = self.bot_checkbox.isChecked()
if new_bot != (self.current_user.bot or False):
profile_data['bot'] = new_bot
new_discoverable = self.discoverable_checkbox.isChecked()
current_discoverable = getattr(self.current_user, 'discoverable', True)
if new_discoverable != current_discoverable:
profile_data['discoverable'] = new_discoverable
# Check fields changes
new_fields = self.fields_widget.get_fields()
current_fields = [{'name': f.name, 'value': f.value} for f in (self.current_user.fields or [])]
if new_fields != current_fields:
profile_data['fields_attributes'] = new_fields
if not profile_data:
QMessageBox.information(self, "No Changes", "No changes were made to your profile.")
return
# Disable save button during update
self.button_box.button(QDialogButtonBox.Save).setEnabled(False)
self.button_box.button(QDialogButtonBox.Save).setText("Saving...")
# Start update thread
self.update_thread = ProfileUpdateThread(self.client, profile_data)
self.update_thread.profile_updated.connect(self.on_profile_updated)
self.update_thread.error_occurred.connect(self.on_update_error)
self.update_thread.start()
self.logger.info(f"Starting profile update with {len(profile_data)} changes")
except Exception as e:
self.logger.error(f"Error preparing profile update: {e}")
AccessibleTextDialog.show_error("Profile Update Error",
"Failed to prepare profile update", str(e), self)
self.sound_manager.play_error()
def on_profile_updated(self, updated_user: User):
"""Handle successful profile update"""
self.current_user = updated_user
# Re-enable save button
self.button_box.button(QDialogButtonBox.Save).setEnabled(True)
self.button_box.button(QDialogButtonBox.Save).setText("Save Profile")
# Emit signal for other components to update
self.profile_updated.emit(updated_user)
# Show success message
QMessageBox.information(self, "Profile Updated", "Your profile has been updated successfully!")
self.logger.info("Profile update completed successfully")
self.sound_manager.play_success()
# Close dialog
self.accept()
def on_update_error(self, error_message: str):
"""Handle profile update error"""
# Re-enable save button
self.button_box.button(QDialogButtonBox.Save).setEnabled(True)
self.button_box.button(QDialogButtonBox.Save).setText("Save Profile")
# Show error
if "422" in error_message:
error_msg = "Invalid profile data. Please check your entries and try again."
elif "403" in error_message:
error_msg = "You don't have permission to update your profile."
elif "timeout" in error_message.lower() or "connection" in error_message.lower():
error_msg = "Network error while updating profile. Please check your connection and try again."
else:
error_msg = f"Failed to update profile: {error_message}"
AccessibleTextDialog.show_error("Profile Update Error", error_msg, "", self)
self.logger.error(f"Profile update failed: {error_message}")
self.sound_manager.play_error()