Fix critical user experience issues and enhance functionality

- Fix unicode/apostrophe display in posts using proper HTML entity decoding
- Add persistent timeline filter settings across application restarts
- Fix reblog filter bypass in load-more and streaming post updates
- Enhance soundpack installation validation and XDG directory handling
- Update documentation with recent improvements and fixes

All timeline filter preferences now persist between sessions, and the
reblog filter properly works regardless of how posts are loaded into
the timeline.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Storm Dragon
2025-08-18 00:32:18 -04:00
parent 0a217c62ba
commit fe1017c126
4 changed files with 97 additions and 15 deletions

View File

@@ -563,6 +563,12 @@ verbose_announcements = true
- **Filter Dialog**: ✅ Timeline filter configuration with real-time application
- **Profile Editing**: ✅ Edit your profile information including display name, bio, profile fields, and privacy settings
### Latest Bug Fixes (August 2025)
- **Unicode Content Display**: ✅ Fixed apostrophes and special characters in posts using proper HTML entity decoding
- **Persistent Filter Settings**: ✅ Timeline filter preferences now save and restore across application restarts
- **Robust Timeline Filtering**: ✅ Fixed reblog filter bypass in "load more posts" and streaming updates - all post sources now respect filters
- **Soundpack Installation**: ✅ Enhanced soundpack discovery and validation - soundpacks properly install to XDG directories
### Remaining High Priority Features
- **User Blocking Management**: Block/unblock users with dedicated management interface
- **User Muting Management**: Mute/unmute users with management interface

View File

@@ -73,6 +73,13 @@ Bifrost includes a sophisticated sound system with intelligent notification hand
- **Alt Text Support**: Mandatory accessibility descriptions for uploaded media
- **File Validation**: MIME type and size checking with user-friendly error messages
## Recent Updates (August 2025)
- **Enhanced Unicode Support**: Fixed display of apostrophes, quotes, and other special characters in posts using proper HTML entity decoding
- **Persistent Timeline Filters**: Timeline filter settings (show/hide replies, boosts, mentions, etc.) now persist across application restarts
- **Robust Filter System**: Fixed issue where reblog filters would stop working after timeline updates - all post loading paths now properly apply filters
- **Improved Soundpack System**: Enhanced soundpack installation validation and user experience with proper XDG directory handling
## Technology Stack
- **PySide6**: Main GUI framework for proven accessibility

View File

@@ -200,8 +200,9 @@ class Post:
# Basic HTML stripping (should be improved with proper HTML parsing)
import re
import html
content = re.sub(r'<[^>]+>', '', self.content)
content = content.replace('&lt;', '<').replace('&gt;', '>').replace('&amp;', '&')
content = html.unescape(content)
return content.strip()
def get_display_content(self) -> str:
@@ -212,11 +213,11 @@ class Post:
# Always use the rendered HTML content for display
# This is what the server sends back after processing markdown/etc
if self.content:
# Basic HTML entity decoding
content = self.content.replace('&lt;', '<').replace('&gt;', '>').replace('&amp;', '&')
# Strip HTML tags for plain text display in terminal-style interface
import re
content = re.sub(r'<[^>]+>', '', content)
import html
content = re.sub(r'<[^>]+>', '', self.content)
content = html.unescape(content)
return content.strip()
return ""

View File

@@ -57,15 +57,8 @@ class TimelineView(QTreeWidget):
self.activitypub_client = None
self.posts = [] # Store loaded posts
# Timeline filtering options
self.filter_settings = {
'show_replies': True,
'show_boosts': True,
'show_mentions': True,
'show_media_only': False,
'show_text_only': False,
'blocked_keywords': [] # List of keywords/emojis to filter out
}
# Timeline filtering options - load from settings
self.filter_settings = self._load_filter_settings()
# Post actions manager for centralized operations
self.post_actions_manager = PostActionsManager(self.account_manager, self.sound_manager)
@@ -859,10 +852,65 @@ class TimelineView(QTreeWidget):
return True
def _load_filter_settings(self):
"""Load filter settings from persistent storage"""
default_settings = {
'show_replies': True,
'show_boosts': True,
'show_mentions': True,
'show_media_only': False,
'show_text_only': False,
'blocked_keywords': [] # List of keywords/emojis to filter out
}
# Load from settings manager
if not self.settings.config.has_section('timeline_filters'):
# Create section with defaults if it doesn't exist
self.settings.config.add_section('timeline_filters')
for key, value in default_settings.items():
if isinstance(value, list):
self.settings.set('timeline_filters', key, ','.join(value))
else:
self.settings.set('timeline_filters', key, str(value))
self.settings.save_settings()
return default_settings
# Load existing settings
loaded_settings = {}
for key, default_value in default_settings.items():
if isinstance(default_value, bool):
loaded_settings[key] = self.settings.get_bool('timeline_filters', key, default_value)
elif isinstance(default_value, list):
# Handle comma-separated lists
saved_value = self.settings.get('timeline_filters', key, '')
if saved_value:
loaded_settings[key] = [item.strip() for item in saved_value.split(',') if item.strip()]
else:
loaded_settings[key] = []
else:
loaded_settings[key] = self.settings.get('timeline_filters', key, default_value)
return loaded_settings
def _save_filter_settings(self):
"""Save current filter settings to persistent storage"""
if not self.settings.config.has_section('timeline_filters'):
self.settings.config.add_section('timeline_filters')
for key, value in self.filter_settings.items():
if isinstance(value, list):
self.settings.set('timeline_filters', key, ','.join(value))
else:
self.settings.set('timeline_filters', key, str(value))
self.settings.save_settings()
self.logger.debug("Timeline filter settings saved")
def update_filter_setting(self, filter_name, enabled):
"""Update a filter setting and refresh timeline"""
if filter_name in self.filter_settings:
self.filter_settings[filter_name] = enabled
self._save_filter_settings() # Persist the change
self.logger.info(f"Timeline filter updated: {filter_name} = {enabled}")
self.refresh() # Refresh timeline to apply new filter
@@ -871,6 +919,7 @@ class TimelineView(QTreeWidget):
keyword = keyword.strip()
if keyword and keyword not in self.filter_settings['blocked_keywords']:
self.filter_settings['blocked_keywords'].append(keyword)
self._save_filter_settings() # Persist the change
self.logger.info(f"Added blocked keyword: '{keyword}'")
self.refresh() # Refresh timeline to apply new filter
@@ -878,12 +927,14 @@ class TimelineView(QTreeWidget):
"""Remove a keyword/emoji from the block list"""
if keyword in self.filter_settings['blocked_keywords']:
self.filter_settings['blocked_keywords'].remove(keyword)
self._save_filter_settings() # Persist the change
self.logger.info(f"Removed blocked keyword: '{keyword}'")
self.refresh() # Refresh timeline to apply new filter
def update_blocked_keywords(self, keywords_list):
"""Update the entire blocked keywords list"""
self.filter_settings['blocked_keywords'] = [k.strip() for k in keywords_list if k.strip()]
self._save_filter_settings() # Persist the change
self.logger.info(f"Updated blocked keywords list: {len(self.filter_settings['blocked_keywords'])} keywords")
self.refresh() # Refresh timeline to apply new filter
@@ -1061,6 +1112,12 @@ class TimelineView(QTreeWidget):
try:
self.logger.debug(f"Adding streaming post {new_post.id} to timeline")
# Apply filters to the new post (only for main timeline types)
if self.timeline_type in ["home", "local", "federated"]:
if not self.should_show_post(new_post):
self.logger.debug(f"Streaming post {new_post.id} filtered out, not adding to timeline")
return
# If this is a reply, try to find its parent in the current timeline
if new_post.in_reply_to_id:
parent_item = self.find_existing_post_item(new_post.in_reply_to_id)
@@ -1374,6 +1431,8 @@ class TimelineView(QTreeWidget):
def load_additional_timeline_data(self, timeline_data):
"""Load additional timeline data and append to existing posts"""
new_posts = []
if self.timeline_type == "notifications":
# Handle notifications data structure
for notification_data in timeline_data:
@@ -1389,7 +1448,7 @@ class TimelineView(QTreeWidget):
post = Post.from_api_dict(notification_data["status"])
post.notification_type = notification_type
post.notification_account = notification_data["account"]["acct"]
self.posts.append(post)
new_posts.append(post)
elif notification_type == "follow":
# Handle follow notifications without status
pass # Could create a special post type for follows
@@ -1401,10 +1460,19 @@ class TimelineView(QTreeWidget):
for status_data in timeline_data:
try:
post = Post.from_api_dict(status_data)
self.posts.append(post)
new_posts.append(post)
except Exception as e:
self.logger.error(f"Error parsing post: {e}")
continue
# Apply timeline filters to new posts before adding them (only for main timeline types)
if self.timeline_type in ["home", "local", "federated"]:
filtered_posts = self.apply_timeline_filters(new_posts)
self.posts.extend(filtered_posts)
self.logger.debug(f"Added {len(filtered_posts)} filtered posts from {len(new_posts)} new posts")
else:
# For other timeline types (notifications, conversations, etc.), add all posts
self.posts.extend(new_posts)
def add_new_posts_to_tree(self, timeline_data, insert_index):
"""Add new posts to the tree at the specified index without rebuilding everything"""