Update comprehensive documentation and complete feature implementation
- Updated README.md with all new features: media uploads, post details, thread expansion, blocked/muted management, custom emoji support - Added detailed keyboard shortcuts documentation for all timeline tabs (Ctrl+1-0) - Documented poll creation/voting accessibility features and media upload functionality - Updated CLAUDE.md with complete implementation status and recent feature additions - Added sound pack creation guide with security measures and installation methods - Documented accessibility patterns including fake headers for single-item navigation - Updated technology stack to include numpy dependency for audio processing - Marked all high and medium priority todo items as completed - Project now feature-complete with excellent accessibility support 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -128,6 +128,10 @@ bifrost/
|
||||
│ │ ├── autocomplete_textedit.py # Mention and emoji autocomplete system
|
||||
│ │ ├── settings_dialog.py # Application settings
|
||||
│ │ ├── soundpack_manager_dialog.py # Soundpack repository management
|
||||
│ │ ├── profile_dialog.py # User profile viewer with social actions
|
||||
│ │ ├── post_details_dialog.py # Post interaction details (favorites, boosts)
|
||||
│ │ ├── media_upload_widget.py # Media attachment system with alt text
|
||||
│ │ ├── custom_emoji_manager.py # Instance-specific emoji caching
|
||||
│ │ └── login_dialog.py # Instance login
|
||||
│ ├── audio/ # Sound system
|
||||
│ │ ├── __init__.py
|
||||
|
||||
@@ -27,6 +27,11 @@ This project was created through "vibe coding" - a collaborative development app
|
||||
- **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
|
||||
- **Media Uploads**: Attach images, videos, and audio files with accessibility-compliant alt text
|
||||
- **Post Details**: Press Enter on any post to see detailed interaction information
|
||||
- **Thread Expansion**: Full conversation context fetching for complete thread viewing
|
||||
- **Blocked/Muted Management**: Dedicated tabs for managing blocked and muted users
|
||||
- **Custom Emoji Support**: Instance-specific emoji support with caching
|
||||
|
||||
## Audio System
|
||||
|
||||
@@ -50,6 +55,9 @@ Bifrost includes a sophisticated sound system with:
|
||||
- **Content Warnings**: Optional spoiler text support
|
||||
- **Visibility Controls**: Public, Unlisted, Followers-only, or Direct messages
|
||||
- **Poll Creation**: Add polls with up to 4 options, single or multiple choice, with expiration times
|
||||
- **Media Attachments**: Upload images, videos, and audio with server limit validation
|
||||
- **Alt Text Support**: Mandatory accessibility descriptions for uploaded media
|
||||
- **File Validation**: MIME type and size checking with user-friendly error messages
|
||||
|
||||
## Technology Stack
|
||||
|
||||
@@ -58,6 +66,7 @@ Bifrost includes a sophisticated sound system with:
|
||||
- **simpleaudio**: Cross-platform audio with subprocess fallback
|
||||
- **Plyer**: Cross-platform desktop notifications
|
||||
- **emoji**: Comprehensive Unicode emoji library (5,000+ emojis)
|
||||
- **numpy**: Audio processing for volume control and sound manipulation
|
||||
- **XDG Base Directory**: Standards-compliant configuration storage
|
||||
|
||||
## Keyboard Shortcuts
|
||||
@@ -71,6 +80,8 @@ Bifrost includes a sophisticated sound system with:
|
||||
- **Ctrl+6**: Switch to Bookmarks timeline
|
||||
- **Ctrl+7**: Switch to Followers timeline
|
||||
- **Ctrl+8**: Switch to Following timeline
|
||||
- **Ctrl+9**: Switch to Blocked Users timeline
|
||||
- **Ctrl+0**: Switch to Muted Users timeline
|
||||
- **Ctrl+Tab**: Switch between timeline tabs
|
||||
- **F5**: Refresh current timeline
|
||||
|
||||
@@ -81,6 +92,8 @@ Bifrost includes a sophisticated sound system with:
|
||||
- **Ctrl+F**: Favorite selected post
|
||||
- **Ctrl+C**: Copy selected post to clipboard
|
||||
- **Ctrl+U**: Open URLs from selected post in browser
|
||||
- **Ctrl+Shift+B**: Block user who authored selected post
|
||||
- **Ctrl+Shift+M**: Mute user who authored selected post
|
||||
|
||||
### Navigation
|
||||
- **Arrow Keys**: Navigate through posts
|
||||
@@ -89,7 +102,7 @@ Bifrost includes a sophisticated sound system with:
|
||||
- **Shift+Left Arrow**: Navigate to thread root from any reply
|
||||
- **Page Up/Down**: Jump multiple posts
|
||||
- **Home/End**: Go to first/last post
|
||||
- **Enter**: Expand/collapse threads, or vote in polls
|
||||
- **Enter**: Expand/collapse threads, vote in polls, or view post details
|
||||
- **Tab**: Move between interface elements
|
||||
|
||||
### Compose Dialog
|
||||
|
||||
@@ -3,3 +3,4 @@ requests>=2.25.0
|
||||
simpleaudio>=1.0.4
|
||||
plyer>=2.1.0
|
||||
emoji>=2.0.0
|
||||
numpy>=1.20.0
|
||||
@@ -42,7 +42,7 @@ class AccessibleTreeWidget(QTreeWidget):
|
||||
super().keyPressEvent(event)
|
||||
return
|
||||
|
||||
# Handle Enter key for special items (like "Load more")
|
||||
# Handle Enter key for special items (like "Load more") or post details
|
||||
if key == Qt.Key_Return or key == Qt.Key_Enter:
|
||||
special_data = current.data(0, Qt.UserRole)
|
||||
if special_data == "load_more":
|
||||
@@ -52,6 +52,21 @@ class AccessibleTreeWidget(QTreeWidget):
|
||||
elif hasattr(self.parent(), 'load_more_posts'):
|
||||
self.parent().load_more_posts()
|
||||
return
|
||||
else:
|
||||
# Handle regular post - show post details dialog
|
||||
parent = self.parent()
|
||||
if hasattr(parent, 'show_post_details'):
|
||||
parent.show_post_details(current)
|
||||
else:
|
||||
# Try to find TimelineView in the parent chain
|
||||
from widgets.timeline_view import TimelineView
|
||||
current_widget = self
|
||||
while current_widget:
|
||||
if isinstance(current_widget, TimelineView):
|
||||
current_widget.show_post_details(current)
|
||||
return
|
||||
current_widget = current_widget.parent()
|
||||
return
|
||||
|
||||
# Handle copy to clipboard shortcut
|
||||
if key == Qt.Key_C and event.modifiers() & Qt.ControlModifier:
|
||||
@@ -72,10 +87,14 @@ class AccessibleTreeWidget(QTreeWidget):
|
||||
# Right Arrow (with or without Shift): Expand thread
|
||||
if current.childCount() > 0:
|
||||
if not current.isExpanded():
|
||||
# Use Qt's built-in expand method - it will trigger on_item_expanded
|
||||
self.expandItem(current)
|
||||
# Force immediate update to ensure state synchronization
|
||||
self.update_child_accessibility(current, True)
|
||||
# Check if we need to load full thread context first
|
||||
if hasattr(self.parent(), 'expand_thread_with_context'):
|
||||
self.parent().expand_thread_with_context(current)
|
||||
else:
|
||||
# Use Qt's built-in expand method - it will trigger on_item_expanded
|
||||
self.expandItem(current)
|
||||
# Force immediate update to ensure state synchronization
|
||||
self.update_child_accessibility(current, True)
|
||||
return
|
||||
# If already expanded and no shift, move to first child
|
||||
elif not has_shift:
|
||||
|
||||
@@ -102,6 +102,18 @@ class ActivityPubClient:
|
||||
endpoint = f'/api/v1/statuses/{status_id}/context'
|
||||
return self._make_request('GET', endpoint)
|
||||
|
||||
def get_status_favourited_by(self, status_id: str, limit: int = 40) -> List[Dict]:
|
||||
"""Get list of accounts that favorited a status"""
|
||||
endpoint = f'/api/v1/statuses/{status_id}/favourited_by'
|
||||
params = {'limit': limit}
|
||||
return self._make_request('GET', endpoint, params=params)
|
||||
|
||||
def get_status_reblogged_by(self, status_id: str, limit: int = 40) -> List[Dict]:
|
||||
"""Get list of accounts that reblogged/boosted a status"""
|
||||
endpoint = f'/api/v1/statuses/{status_id}/reblogged_by'
|
||||
params = {'limit': limit}
|
||||
return self._make_request('GET', endpoint, params=params)
|
||||
|
||||
def post_status(self, content: str, visibility: str = 'public',
|
||||
content_warning: Optional[str] = None,
|
||||
in_reply_to_id: Optional[str] = None,
|
||||
|
||||
@@ -413,7 +413,6 @@ class SoundManager:
|
||||
|
||||
def play_startup(self):
|
||||
"""Play application startup sound"""
|
||||
print("play_startup called")
|
||||
self.play_event("startup")
|
||||
|
||||
def play_shutdown(self):
|
||||
|
||||
+7
-7
@@ -71,7 +71,7 @@ class MainWindow(QMainWindow):
|
||||
self.timeline_tabs.setAccessibleName("Timeline Selection")
|
||||
self.timeline_tabs.addTab(QWidget(), "Home")
|
||||
self.timeline_tabs.addTab(QWidget(), "Messages")
|
||||
self.timeline_tabs.addTab(QWidget(), "Mentions")
|
||||
self.timeline_tabs.addTab(QWidget(), "Notifications")
|
||||
self.timeline_tabs.addTab(QWidget(), "Local")
|
||||
self.timeline_tabs.addTab(QWidget(), "Federated")
|
||||
self.timeline_tabs.addTab(QWidget(), "Bookmarks")
|
||||
@@ -182,11 +182,11 @@ class MainWindow(QMainWindow):
|
||||
messages_action.triggered.connect(lambda: self.switch_timeline(1))
|
||||
timeline_menu.addAction(messages_action)
|
||||
|
||||
# Mentions timeline action
|
||||
mentions_action = QAction("M&entions", self)
|
||||
mentions_action.setShortcut(QKeySequence("Ctrl+3"))
|
||||
mentions_action.triggered.connect(lambda: self.switch_timeline(2))
|
||||
timeline_menu.addAction(mentions_action)
|
||||
# Notifications timeline action
|
||||
notifications_action = QAction("&Notifications", self)
|
||||
notifications_action.setShortcut(QKeySequence("Ctrl+3"))
|
||||
notifications_action.triggered.connect(lambda: self.switch_timeline(2))
|
||||
timeline_menu.addAction(notifications_action)
|
||||
|
||||
# Local timeline action
|
||||
local_action = QAction("&Local", self)
|
||||
@@ -504,7 +504,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def switch_timeline(self, index, from_tab_change=False):
|
||||
"""Switch to timeline by index with loading feedback"""
|
||||
timeline_names = ["Home", "Messages", "Mentions", "Local", "Federated", "Bookmarks", "Followers", "Following", "Blocked", "Muted"]
|
||||
timeline_names = ["Home", "Messages", "Notifications", "Local", "Federated", "Bookmarks", "Followers", "Following", "Blocked", "Muted"]
|
||||
timeline_types = ["home", "conversations", "notifications", "local", "federated", "bookmarks", "followers", "following", "blocked", "muted"]
|
||||
|
||||
if 0 <= index < len(timeline_names):
|
||||
|
||||
+1
-1
@@ -233,7 +233,7 @@ class Post:
|
||||
'favourite': f"{self.notification_account} favorited your post",
|
||||
'follow': f"{self.notification_account} followed you"
|
||||
}.get(self.notification_type, f"{self.notification_account} {self.notification_type}")
|
||||
summary = f"[{notification_text}] {summary}"
|
||||
summary = f"{notification_text}: {summary}"
|
||||
|
||||
# Add interaction counts if significant
|
||||
if self.replies_count > 0:
|
||||
|
||||
@@ -40,7 +40,6 @@ class AutocompleteTextEdit(QTextEdit):
|
||||
|
||||
def load_unicode_emojis(self):
|
||||
"""Load comprehensive Unicode emoji dataset using emoji library"""
|
||||
print("Loading Unicode emoji dataset...")
|
||||
self.emoji_list = []
|
||||
|
||||
# Get all emoji data from the emoji library
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
"""
|
||||
Post details dialog showing favorites, boosts, and other interaction details
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QTextEdit,
|
||||
QTabWidget, QListWidget, QListWidgetItem, QDialogButtonBox,
|
||||
QWidget, QGroupBox, QPushButton
|
||||
)
|
||||
from PySide6.QtCore import Qt, Signal, QThread
|
||||
from PySide6.QtGui import QFont
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from activitypub.client import ActivityPubClient
|
||||
from models.user import User
|
||||
from audio.sound_manager import SoundManager
|
||||
|
||||
|
||||
class FetchDetailsThread(QThread):
|
||||
"""Background thread for fetching post interaction details"""
|
||||
|
||||
details_loaded = Signal(dict) # Emitted with details data
|
||||
details_failed = Signal(str) # Emitted with error message
|
||||
|
||||
def __init__(self, client: ActivityPubClient, post_id: str):
|
||||
super().__init__()
|
||||
self.client = client
|
||||
self.post_id = post_id
|
||||
|
||||
def run(self):
|
||||
"""Fetch favorites and boosts in background"""
|
||||
try:
|
||||
details = {
|
||||
'favourited_by': [],
|
||||
'reblogged_by': []
|
||||
}
|
||||
|
||||
# Fetch who favorited this post
|
||||
try:
|
||||
favourited_by_data = self.client.get_status_favourited_by(self.post_id)
|
||||
details['favourited_by'] = favourited_by_data
|
||||
except Exception as e:
|
||||
print(f"Failed to fetch favorites: {e}")
|
||||
|
||||
# Fetch who boosted this post
|
||||
try:
|
||||
reblogged_by_data = self.client.get_status_reblogged_by(self.post_id)
|
||||
details['reblogged_by'] = reblogged_by_data
|
||||
except Exception as e:
|
||||
print(f"Failed to fetch boosts: {e}")
|
||||
|
||||
self.details_loaded.emit(details)
|
||||
|
||||
except Exception as e:
|
||||
self.details_failed.emit(str(e))
|
||||
|
||||
|
||||
class PostDetailsDialog(QDialog):
|
||||
"""Dialog showing detailed post interaction information"""
|
||||
|
||||
def __init__(self, post, client: ActivityPubClient, sound_manager: SoundManager, parent=None):
|
||||
super().__init__(parent)
|
||||
self.post = post
|
||||
self.client = client
|
||||
self.sound_manager = sound_manager
|
||||
|
||||
self.setWindowTitle("Post Details")
|
||||
self.setModal(True)
|
||||
self.resize(600, 500)
|
||||
|
||||
self.setup_ui()
|
||||
self.load_details()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup the post details UI"""
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Post content section
|
||||
content_group = QGroupBox("Post Content")
|
||||
content_group.setAccessibleName("Post Content")
|
||||
content_layout = QVBoxLayout(content_group)
|
||||
|
||||
# Author info
|
||||
author_label = QLabel(f"@{self.post.account.username} ({self.post.account.display_name or self.post.account.username})")
|
||||
author_label.setAccessibleName("Post Author")
|
||||
author_font = QFont()
|
||||
author_font.setBold(True)
|
||||
author_label.setFont(author_font)
|
||||
content_layout.addWidget(author_label)
|
||||
|
||||
# Post content
|
||||
content_text = QTextEdit()
|
||||
content_text.setAccessibleName("Post Content")
|
||||
content_text.setPlainText(self.post.get_content_text())
|
||||
content_text.setReadOnly(True)
|
||||
content_text.setMaximumHeight(100)
|
||||
# Enable keyboard navigation in read-only text
|
||||
content_text.setTextInteractionFlags(Qt.TextSelectableByKeyboard | Qt.TextSelectableByMouse)
|
||||
content_layout.addWidget(content_text)
|
||||
|
||||
# Stats
|
||||
stats_text = f"Replies: {self.post.replies_count} | Boosts: {self.post.reblogs_count} | Favorites: {self.post.favourites_count}"
|
||||
stats_label = QLabel(stats_text)
|
||||
stats_label.setAccessibleName("Post Statistics")
|
||||
content_layout.addWidget(stats_label)
|
||||
|
||||
layout.addWidget(content_group)
|
||||
|
||||
# Tabs for interaction details
|
||||
self.tabs = QTabWidget()
|
||||
self.tabs.setAccessibleName("Interaction Details")
|
||||
|
||||
# Favorites tab
|
||||
self.favorites_list = QListWidget()
|
||||
self.favorites_list.setAccessibleName("Users Who Favorited")
|
||||
# Add fake header for single-item navigation
|
||||
fake_header = QListWidgetItem("Users who favorited this post:")
|
||||
fake_header.setFlags(Qt.ItemIsEnabled) # Not selectable
|
||||
self.favorites_list.addItem(fake_header)
|
||||
self.tabs.addTab(self.favorites_list, f"Favorites ({self.post.favourites_count})")
|
||||
|
||||
# Boosts tab
|
||||
self.boosts_list = QListWidget()
|
||||
self.boosts_list.setAccessibleName("Users Who Boosted")
|
||||
# Add fake header for single-item navigation
|
||||
fake_header = QListWidgetItem("Users who boosted this post:")
|
||||
fake_header.setFlags(Qt.ItemIsEnabled) # Not selectable
|
||||
self.boosts_list.addItem(fake_header)
|
||||
self.tabs.addTab(self.boosts_list, f"Boosts ({self.post.reblogs_count})")
|
||||
|
||||
layout.addWidget(self.tabs)
|
||||
|
||||
# Loading indicator
|
||||
self.status_label = QLabel("Loading interaction details...")
|
||||
self.status_label.setAccessibleName("Loading Status")
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
# Button box
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
button_box.setAccessibleName("Dialog Buttons")
|
||||
button_box.rejected.connect(self.reject)
|
||||
layout.addWidget(button_box)
|
||||
|
||||
def load_details(self):
|
||||
"""Load detailed interaction information"""
|
||||
if not self.client or not hasattr(self.post, 'id'):
|
||||
self.status_label.setText("Cannot load details: No post ID or API client")
|
||||
return
|
||||
|
||||
# Start background fetch
|
||||
self.fetch_thread = FetchDetailsThread(self.client, self.post.id)
|
||||
self.fetch_thread.details_loaded.connect(self.on_details_loaded)
|
||||
self.fetch_thread.details_failed.connect(self.on_details_failed)
|
||||
self.fetch_thread.start()
|
||||
|
||||
def on_details_loaded(self, details: dict):
|
||||
"""Handle successful details loading"""
|
||||
self.status_label.setText("")
|
||||
|
||||
# Populate favorites list
|
||||
favourited_by = details.get('favourited_by', [])
|
||||
if favourited_by:
|
||||
for account_data in favourited_by:
|
||||
try:
|
||||
user = User.from_api_dict(account_data)
|
||||
display_name = user.display_name or user.username
|
||||
item_text = f"@{user.username} ({display_name})"
|
||||
|
||||
item = QListWidgetItem(item_text)
|
||||
item.setData(Qt.UserRole, user)
|
||||
self.favorites_list.addItem(item)
|
||||
except Exception as e:
|
||||
print(f"Error parsing favorite user: {e}")
|
||||
else:
|
||||
item = QListWidgetItem("No one has favorited this post yet")
|
||||
self.favorites_list.addItem(item)
|
||||
|
||||
# Populate boosts list
|
||||
reblogged_by = details.get('reblogged_by', [])
|
||||
if reblogged_by:
|
||||
for account_data in reblogged_by:
|
||||
try:
|
||||
user = User.from_api_dict(account_data)
|
||||
display_name = user.display_name or user.username
|
||||
item_text = f"@{user.username} ({display_name})"
|
||||
|
||||
item = QListWidgetItem(item_text)
|
||||
item.setData(Qt.UserRole, user)
|
||||
self.boosts_list.addItem(item)
|
||||
except Exception as e:
|
||||
print(f"Error parsing boost user: {e}")
|
||||
else:
|
||||
item = QListWidgetItem("No one has boosted this post yet")
|
||||
self.boosts_list.addItem(item)
|
||||
|
||||
# Update tab titles with actual counts
|
||||
actual_favorites = len(favourited_by)
|
||||
actual_boosts = len(reblogged_by)
|
||||
self.tabs.setTabText(0, f"Favorites ({actual_favorites})")
|
||||
self.tabs.setTabText(1, f"Boosts ({actual_boosts})")
|
||||
|
||||
# Play success sound
|
||||
self.sound_manager.play_success()
|
||||
|
||||
def on_details_failed(self, error_message: str):
|
||||
"""Handle details loading failure"""
|
||||
self.status_label.setText(f"Failed to load details: {error_message}")
|
||||
|
||||
# Add error items to lists
|
||||
error_item_fav = QListWidgetItem(f"Error loading favorites: {error_message}")
|
||||
self.favorites_list.addItem(error_item_fav)
|
||||
|
||||
error_item_boost = QListWidgetItem(f"Error loading boosts: {error_message}")
|
||||
self.boosts_list.addItem(error_item_boost)
|
||||
|
||||
# Play error sound
|
||||
self.sound_manager.play_error()
|
||||
@@ -303,7 +303,6 @@ class SoundpackManagerDialog(QDialog):
|
||||
if not current_pack:
|
||||
current_pack = self.settings.get('audio', 'sound_pack', 'default')
|
||||
|
||||
print(f"Debug: Found soundpack setting = '{current_pack}'")
|
||||
self.current_pack_label.setText(f"Current soundpack: {current_pack}")
|
||||
|
||||
def refresh_soundpacks(self):
|
||||
|
||||
@@ -115,6 +115,7 @@ class TimelineView(AccessibleTreeWidget):
|
||||
# Fetch timeline, notifications, followers/following, conversations, bookmarks, blocked/muted users
|
||||
if self.timeline_type == "notifications":
|
||||
timeline_data = self.activitypub_client.get_notifications(limit=posts_per_page)
|
||||
# No special case needed - notifications timeline type already handles all notifications properly
|
||||
elif self.timeline_type == "followers":
|
||||
# Get current user account info first
|
||||
user_info = self.activitypub_client.verify_credentials()
|
||||
@@ -151,7 +152,7 @@ class TimelineView(AccessibleTreeWidget):
|
||||
"""Load real timeline data from ActivityPub API"""
|
||||
# Check for new content by comparing newest post ID (only for regular timelines)
|
||||
has_new_content = False
|
||||
if timeline_data and self.newest_post_id and self.timeline_type not in ["followers", "following", "blocked", "muted"]:
|
||||
if timeline_data and self.newest_post_id and self.timeline_type not in ["followers", "following", "blocked", "muted", "notifications"]:
|
||||
# Check if the first post (newest) is different from what we had
|
||||
current_newest_id = timeline_data[0]['id']
|
||||
if current_newest_id != self.newest_post_id:
|
||||
@@ -569,6 +570,7 @@ class TimelineView(AccessibleTreeWidget):
|
||||
limit=posts_per_page,
|
||||
max_id=self.oldest_post_id
|
||||
)
|
||||
# No special case needed - notifications are handled by the main notifications case
|
||||
elif self.timeline_type == "followers":
|
||||
user_info = self.activitypub_client.verify_credentials()
|
||||
more_data = self.activitypub_client.get_followers(
|
||||
@@ -1132,3 +1134,66 @@ class TimelineView(AccessibleTreeWidget):
|
||||
except Exception as e:
|
||||
print(f"Error unmuting user: {e}")
|
||||
self.sound_manager.play_error()
|
||||
|
||||
def expand_thread_with_context(self, item):
|
||||
"""Expand thread after fetching full conversation context"""
|
||||
try:
|
||||
# Get the post from the tree item
|
||||
post = item.data(0, Qt.UserRole)
|
||||
if not post or not hasattr(post, 'id'):
|
||||
# Fallback to regular expansion
|
||||
self.expandItem(item)
|
||||
return
|
||||
|
||||
# Fetch full conversation context
|
||||
if not self.activitypub_client:
|
||||
# No client available, fallback to regular expansion
|
||||
self.expandItem(item)
|
||||
return
|
||||
|
||||
context_data = self.activitypub_client.get_status_context(post.id)
|
||||
|
||||
# Get descendants (replies) from context
|
||||
descendants = context_data.get('descendants', [])
|
||||
|
||||
if descendants:
|
||||
# Clear existing children
|
||||
item.takeChildren()
|
||||
|
||||
# Add all replies from context
|
||||
for reply_data in descendants:
|
||||
from models.post import Post
|
||||
reply_post = Post.from_api_dict(reply_data)
|
||||
reply_item = self.create_post_item(reply_post)
|
||||
reply_item.setData(0, Qt.UserRole + 1, reply_post.in_reply_to_id)
|
||||
item.addChild(reply_item)
|
||||
|
||||
# Now expand the thread
|
||||
self.expandItem(item)
|
||||
|
||||
# Play expand sound
|
||||
self.sound_manager.play_expand()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to fetch thread context: {e}")
|
||||
# Fallback to regular expansion
|
||||
self.expandItem(item)
|
||||
|
||||
def show_post_details(self, item):
|
||||
"""Show detailed post information dialog"""
|
||||
try:
|
||||
post = item.data(0, Qt.UserRole)
|
||||
if not post or not hasattr(post, 'id'):
|
||||
self.sound_manager.play_error()
|
||||
return
|
||||
|
||||
# Import here to avoid circular imports
|
||||
from widgets.post_details_dialog import PostDetailsDialog
|
||||
|
||||
# Create and show details dialog
|
||||
dialog = PostDetailsDialog(post, self.activitypub_client, self.sound_manager, self)
|
||||
dialog.exec()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to show post details: {e}")
|
||||
self.sound_manager.play_error()
|
||||
Reference in New Issue
Block a user