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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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('<', '<').replace('>', '>').replace('&', '&')
|
||||
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('<', '<').replace('>', '>').replace('&', '&')
|
||||
# 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 ""
|
||||
|
||||
@@ -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"""
|
||||
|
||||
Reference in New Issue
Block a user