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
|
- **Filter Dialog**: ✅ Timeline filter configuration with real-time application
|
||||||
- **Profile Editing**: ✅ Edit your profile information including display name, bio, profile fields, and privacy settings
|
- **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
|
### Remaining High Priority Features
|
||||||
- **User Blocking Management**: Block/unblock users with dedicated management interface
|
- **User Blocking Management**: Block/unblock users with dedicated management interface
|
||||||
- **User Muting Management**: Mute/unmute users with 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
|
- **Alt Text Support**: Mandatory accessibility descriptions for uploaded media
|
||||||
- **File Validation**: MIME type and size checking with user-friendly error messages
|
- **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
|
## Technology Stack
|
||||||
|
|
||||||
- **PySide6**: Main GUI framework for proven accessibility
|
- **PySide6**: Main GUI framework for proven accessibility
|
||||||
|
|||||||
+5
-4
@@ -200,8 +200,9 @@ class Post:
|
|||||||
|
|
||||||
# Basic HTML stripping (should be improved with proper HTML parsing)
|
# Basic HTML stripping (should be improved with proper HTML parsing)
|
||||||
import re
|
import re
|
||||||
|
import html
|
||||||
content = re.sub(r'<[^>]+>', '', self.content)
|
content = re.sub(r'<[^>]+>', '', self.content)
|
||||||
content = content.replace('<', '<').replace('>', '>').replace('&', '&')
|
content = html.unescape(content)
|
||||||
return content.strip()
|
return content.strip()
|
||||||
|
|
||||||
def get_display_content(self) -> str:
|
def get_display_content(self) -> str:
|
||||||
@@ -212,11 +213,11 @@ class Post:
|
|||||||
# Always use the rendered HTML content for display
|
# Always use the rendered HTML content for display
|
||||||
# This is what the server sends back after processing markdown/etc
|
# This is what the server sends back after processing markdown/etc
|
||||||
if self.content:
|
if self.content:
|
||||||
# Basic HTML entity decoding
|
|
||||||
content = self.content.replace('<', '<').replace('>', '>').replace('&', '&')
|
|
||||||
# Strip HTML tags for plain text display in terminal-style interface
|
# Strip HTML tags for plain text display in terminal-style interface
|
||||||
import re
|
import re
|
||||||
content = re.sub(r'<[^>]+>', '', content)
|
import html
|
||||||
|
content = re.sub(r'<[^>]+>', '', self.content)
|
||||||
|
content = html.unescape(content)
|
||||||
return content.strip()
|
return content.strip()
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -57,15 +57,8 @@ class TimelineView(QTreeWidget):
|
|||||||
self.activitypub_client = None
|
self.activitypub_client = None
|
||||||
self.posts = [] # Store loaded posts
|
self.posts = [] # Store loaded posts
|
||||||
|
|
||||||
# Timeline filtering options
|
# Timeline filtering options - load from settings
|
||||||
self.filter_settings = {
|
self.filter_settings = self._load_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
|
|
||||||
}
|
|
||||||
|
|
||||||
# Post actions manager for centralized operations
|
# Post actions manager for centralized operations
|
||||||
self.post_actions_manager = PostActionsManager(self.account_manager, self.sound_manager)
|
self.post_actions_manager = PostActionsManager(self.account_manager, self.sound_manager)
|
||||||
@@ -859,10 +852,65 @@ class TimelineView(QTreeWidget):
|
|||||||
|
|
||||||
return True
|
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):
|
def update_filter_setting(self, filter_name, enabled):
|
||||||
"""Update a filter setting and refresh timeline"""
|
"""Update a filter setting and refresh timeline"""
|
||||||
if filter_name in self.filter_settings:
|
if filter_name in self.filter_settings:
|
||||||
self.filter_settings[filter_name] = enabled
|
self.filter_settings[filter_name] = enabled
|
||||||
|
self._save_filter_settings() # Persist the change
|
||||||
self.logger.info(f"Timeline filter updated: {filter_name} = {enabled}")
|
self.logger.info(f"Timeline filter updated: {filter_name} = {enabled}")
|
||||||
self.refresh() # Refresh timeline to apply new filter
|
self.refresh() # Refresh timeline to apply new filter
|
||||||
|
|
||||||
@@ -871,6 +919,7 @@ class TimelineView(QTreeWidget):
|
|||||||
keyword = keyword.strip()
|
keyword = keyword.strip()
|
||||||
if keyword and keyword not in self.filter_settings['blocked_keywords']:
|
if keyword and keyword not in self.filter_settings['blocked_keywords']:
|
||||||
self.filter_settings['blocked_keywords'].append(keyword)
|
self.filter_settings['blocked_keywords'].append(keyword)
|
||||||
|
self._save_filter_settings() # Persist the change
|
||||||
self.logger.info(f"Added blocked keyword: '{keyword}'")
|
self.logger.info(f"Added blocked keyword: '{keyword}'")
|
||||||
self.refresh() # Refresh timeline to apply new filter
|
self.refresh() # Refresh timeline to apply new filter
|
||||||
|
|
||||||
@@ -878,12 +927,14 @@ class TimelineView(QTreeWidget):
|
|||||||
"""Remove a keyword/emoji from the block list"""
|
"""Remove a keyword/emoji from the block list"""
|
||||||
if keyword in self.filter_settings['blocked_keywords']:
|
if keyword in self.filter_settings['blocked_keywords']:
|
||||||
self.filter_settings['blocked_keywords'].remove(keyword)
|
self.filter_settings['blocked_keywords'].remove(keyword)
|
||||||
|
self._save_filter_settings() # Persist the change
|
||||||
self.logger.info(f"Removed blocked keyword: '{keyword}'")
|
self.logger.info(f"Removed blocked keyword: '{keyword}'")
|
||||||
self.refresh() # Refresh timeline to apply new filter
|
self.refresh() # Refresh timeline to apply new filter
|
||||||
|
|
||||||
def update_blocked_keywords(self, keywords_list):
|
def update_blocked_keywords(self, keywords_list):
|
||||||
"""Update the entire blocked keywords list"""
|
"""Update the entire blocked keywords list"""
|
||||||
self.filter_settings['blocked_keywords'] = [k.strip() for k in keywords_list if k.strip()]
|
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.logger.info(f"Updated blocked keywords list: {len(self.filter_settings['blocked_keywords'])} keywords")
|
||||||
self.refresh() # Refresh timeline to apply new filter
|
self.refresh() # Refresh timeline to apply new filter
|
||||||
|
|
||||||
@@ -1061,6 +1112,12 @@ class TimelineView(QTreeWidget):
|
|||||||
try:
|
try:
|
||||||
self.logger.debug(f"Adding streaming post {new_post.id} to timeline")
|
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 this is a reply, try to find its parent in the current timeline
|
||||||
if new_post.in_reply_to_id:
|
if new_post.in_reply_to_id:
|
||||||
parent_item = self.find_existing_post_item(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):
|
def load_additional_timeline_data(self, timeline_data):
|
||||||
"""Load additional timeline data and append to existing posts"""
|
"""Load additional timeline data and append to existing posts"""
|
||||||
|
new_posts = []
|
||||||
|
|
||||||
if self.timeline_type == "notifications":
|
if self.timeline_type == "notifications":
|
||||||
# Handle notifications data structure
|
# Handle notifications data structure
|
||||||
for notification_data in timeline_data:
|
for notification_data in timeline_data:
|
||||||
@@ -1389,7 +1448,7 @@ class TimelineView(QTreeWidget):
|
|||||||
post = Post.from_api_dict(notification_data["status"])
|
post = Post.from_api_dict(notification_data["status"])
|
||||||
post.notification_type = notification_type
|
post.notification_type = notification_type
|
||||||
post.notification_account = notification_data["account"]["acct"]
|
post.notification_account = notification_data["account"]["acct"]
|
||||||
self.posts.append(post)
|
new_posts.append(post)
|
||||||
elif notification_type == "follow":
|
elif notification_type == "follow":
|
||||||
# Handle follow notifications without status
|
# Handle follow notifications without status
|
||||||
pass # Could create a special post type for follows
|
pass # Could create a special post type for follows
|
||||||
@@ -1401,10 +1460,19 @@ class TimelineView(QTreeWidget):
|
|||||||
for status_data in timeline_data:
|
for status_data in timeline_data:
|
||||||
try:
|
try:
|
||||||
post = Post.from_api_dict(status_data)
|
post = Post.from_api_dict(status_data)
|
||||||
self.posts.append(post)
|
new_posts.append(post)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error parsing post: {e}")
|
self.logger.error(f"Error parsing post: {e}")
|
||||||
continue
|
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):
|
def add_new_posts_to_tree(self, timeline_data, insert_index):
|
||||||
"""Add new posts to the tree at the specified index without rebuilding everything"""
|
"""Add new posts to the tree at the specified index without rebuilding everything"""
|
||||||
|
|||||||
Reference in New Issue
Block a user