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
|
│ │ ├── autocomplete_textedit.py # Mention and emoji autocomplete system
|
||||||
│ │ ├── settings_dialog.py # Application settings
|
│ │ ├── settings_dialog.py # Application settings
|
||||||
│ │ ├── soundpack_manager_dialog.py # Soundpack repository management
|
│ │ ├── 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
|
│ │ └── login_dialog.py # Instance login
|
||||||
│ ├── audio/ # Sound system
|
│ ├── audio/ # Sound system
|
||||||
│ │ ├── __init__.py
|
│ │ ├── __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
|
- **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
|
- **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
|
- **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
|
## Audio System
|
||||||
|
|
||||||
@@ -50,6 +55,9 @@ Bifrost includes a sophisticated sound system with:
|
|||||||
- **Content Warnings**: Optional spoiler text support
|
- **Content Warnings**: Optional spoiler text support
|
||||||
- **Visibility Controls**: Public, Unlisted, Followers-only, or Direct messages
|
- **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
|
- **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
|
## Technology Stack
|
||||||
|
|
||||||
@@ -58,6 +66,7 @@ Bifrost includes a sophisticated sound system with:
|
|||||||
- **simpleaudio**: Cross-platform audio with subprocess fallback
|
- **simpleaudio**: Cross-platform audio with subprocess fallback
|
||||||
- **Plyer**: Cross-platform desktop notifications
|
- **Plyer**: Cross-platform desktop notifications
|
||||||
- **emoji**: Comprehensive Unicode emoji library (5,000+ emojis)
|
- **emoji**: Comprehensive Unicode emoji library (5,000+ emojis)
|
||||||
|
- **numpy**: Audio processing for volume control and sound manipulation
|
||||||
- **XDG Base Directory**: Standards-compliant configuration storage
|
- **XDG Base Directory**: Standards-compliant configuration storage
|
||||||
|
|
||||||
## Keyboard Shortcuts
|
## Keyboard Shortcuts
|
||||||
@@ -71,6 +80,8 @@ Bifrost includes a sophisticated sound system with:
|
|||||||
- **Ctrl+6**: Switch to Bookmarks timeline
|
- **Ctrl+6**: Switch to Bookmarks timeline
|
||||||
- **Ctrl+7**: Switch to Followers timeline
|
- **Ctrl+7**: Switch to Followers timeline
|
||||||
- **Ctrl+8**: Switch to Following 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
|
- **Ctrl+Tab**: Switch between timeline tabs
|
||||||
- **F5**: Refresh current timeline
|
- **F5**: Refresh current timeline
|
||||||
|
|
||||||
@@ -81,6 +92,8 @@ Bifrost includes a sophisticated sound system with:
|
|||||||
- **Ctrl+F**: Favorite selected post
|
- **Ctrl+F**: Favorite selected post
|
||||||
- **Ctrl+C**: Copy selected post to clipboard
|
- **Ctrl+C**: Copy selected post to clipboard
|
||||||
- **Ctrl+U**: Open URLs from selected post in browser
|
- **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
|
### Navigation
|
||||||
- **Arrow Keys**: Navigate through posts
|
- **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
|
- **Shift+Left Arrow**: Navigate to thread root from any reply
|
||||||
- **Page Up/Down**: Jump multiple posts
|
- **Page Up/Down**: Jump multiple posts
|
||||||
- **Home/End**: Go to first/last post
|
- **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
|
- **Tab**: Move between interface elements
|
||||||
|
|
||||||
### Compose Dialog
|
### Compose Dialog
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ requests>=2.25.0
|
|||||||
simpleaudio>=1.0.4
|
simpleaudio>=1.0.4
|
||||||
plyer>=2.1.0
|
plyer>=2.1.0
|
||||||
emoji>=2.0.0
|
emoji>=2.0.0
|
||||||
|
numpy>=1.20.0
|
||||||
@@ -42,7 +42,7 @@ class AccessibleTreeWidget(QTreeWidget):
|
|||||||
super().keyPressEvent(event)
|
super().keyPressEvent(event)
|
||||||
return
|
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:
|
if key == Qt.Key_Return or key == Qt.Key_Enter:
|
||||||
special_data = current.data(0, Qt.UserRole)
|
special_data = current.data(0, Qt.UserRole)
|
||||||
if special_data == "load_more":
|
if special_data == "load_more":
|
||||||
@@ -52,6 +52,21 @@ class AccessibleTreeWidget(QTreeWidget):
|
|||||||
elif hasattr(self.parent(), 'load_more_posts'):
|
elif hasattr(self.parent(), 'load_more_posts'):
|
||||||
self.parent().load_more_posts()
|
self.parent().load_more_posts()
|
||||||
return
|
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
|
# Handle copy to clipboard shortcut
|
||||||
if key == Qt.Key_C and event.modifiers() & Qt.ControlModifier:
|
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
|
# Right Arrow (with or without Shift): Expand thread
|
||||||
if current.childCount() > 0:
|
if current.childCount() > 0:
|
||||||
if not current.isExpanded():
|
if not current.isExpanded():
|
||||||
# Use Qt's built-in expand method - it will trigger on_item_expanded
|
# Check if we need to load full thread context first
|
||||||
self.expandItem(current)
|
if hasattr(self.parent(), 'expand_thread_with_context'):
|
||||||
# Force immediate update to ensure state synchronization
|
self.parent().expand_thread_with_context(current)
|
||||||
self.update_child_accessibility(current, True)
|
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
|
return
|
||||||
# If already expanded and no shift, move to first child
|
# If already expanded and no shift, move to first child
|
||||||
elif not has_shift:
|
elif not has_shift:
|
||||||
|
|||||||
@@ -102,6 +102,18 @@ class ActivityPubClient:
|
|||||||
endpoint = f'/api/v1/statuses/{status_id}/context'
|
endpoint = f'/api/v1/statuses/{status_id}/context'
|
||||||
return self._make_request('GET', endpoint)
|
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',
|
def post_status(self, content: str, visibility: str = 'public',
|
||||||
content_warning: Optional[str] = None,
|
content_warning: Optional[str] = None,
|
||||||
in_reply_to_id: Optional[str] = None,
|
in_reply_to_id: Optional[str] = None,
|
||||||
|
|||||||
@@ -413,7 +413,6 @@ class SoundManager:
|
|||||||
|
|
||||||
def play_startup(self):
|
def play_startup(self):
|
||||||
"""Play application startup sound"""
|
"""Play application startup sound"""
|
||||||
print("play_startup called")
|
|
||||||
self.play_event("startup")
|
self.play_event("startup")
|
||||||
|
|
||||||
def play_shutdown(self):
|
def play_shutdown(self):
|
||||||
|
|||||||
+7
-7
@@ -71,7 +71,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.timeline_tabs.setAccessibleName("Timeline Selection")
|
self.timeline_tabs.setAccessibleName("Timeline Selection")
|
||||||
self.timeline_tabs.addTab(QWidget(), "Home")
|
self.timeline_tabs.addTab(QWidget(), "Home")
|
||||||
self.timeline_tabs.addTab(QWidget(), "Messages")
|
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(), "Local")
|
||||||
self.timeline_tabs.addTab(QWidget(), "Federated")
|
self.timeline_tabs.addTab(QWidget(), "Federated")
|
||||||
self.timeline_tabs.addTab(QWidget(), "Bookmarks")
|
self.timeline_tabs.addTab(QWidget(), "Bookmarks")
|
||||||
@@ -182,11 +182,11 @@ class MainWindow(QMainWindow):
|
|||||||
messages_action.triggered.connect(lambda: self.switch_timeline(1))
|
messages_action.triggered.connect(lambda: self.switch_timeline(1))
|
||||||
timeline_menu.addAction(messages_action)
|
timeline_menu.addAction(messages_action)
|
||||||
|
|
||||||
# Mentions timeline action
|
# Notifications timeline action
|
||||||
mentions_action = QAction("M&entions", self)
|
notifications_action = QAction("&Notifications", self)
|
||||||
mentions_action.setShortcut(QKeySequence("Ctrl+3"))
|
notifications_action.setShortcut(QKeySequence("Ctrl+3"))
|
||||||
mentions_action.triggered.connect(lambda: self.switch_timeline(2))
|
notifications_action.triggered.connect(lambda: self.switch_timeline(2))
|
||||||
timeline_menu.addAction(mentions_action)
|
timeline_menu.addAction(notifications_action)
|
||||||
|
|
||||||
# Local timeline action
|
# Local timeline action
|
||||||
local_action = QAction("&Local", self)
|
local_action = QAction("&Local", self)
|
||||||
@@ -504,7 +504,7 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def switch_timeline(self, index, from_tab_change=False):
|
def switch_timeline(self, index, from_tab_change=False):
|
||||||
"""Switch to timeline by index with loading feedback"""
|
"""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"]
|
timeline_types = ["home", "conversations", "notifications", "local", "federated", "bookmarks", "followers", "following", "blocked", "muted"]
|
||||||
|
|
||||||
if 0 <= index < len(timeline_names):
|
if 0 <= index < len(timeline_names):
|
||||||
|
|||||||
+1
-1
@@ -233,7 +233,7 @@ class Post:
|
|||||||
'favourite': f"{self.notification_account} favorited your post",
|
'favourite': f"{self.notification_account} favorited your post",
|
||||||
'follow': f"{self.notification_account} followed you"
|
'follow': f"{self.notification_account} followed you"
|
||||||
}.get(self.notification_type, f"{self.notification_account} {self.notification_type}")
|
}.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
|
# Add interaction counts if significant
|
||||||
if self.replies_count > 0:
|
if self.replies_count > 0:
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ class AutocompleteTextEdit(QTextEdit):
|
|||||||
|
|
||||||
def load_unicode_emojis(self):
|
def load_unicode_emojis(self):
|
||||||
"""Load comprehensive Unicode emoji dataset using emoji library"""
|
"""Load comprehensive Unicode emoji dataset using emoji library"""
|
||||||
print("Loading Unicode emoji dataset...")
|
|
||||||
self.emoji_list = []
|
self.emoji_list = []
|
||||||
|
|
||||||
# Get all emoji data from the emoji library
|
# 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:
|
if not current_pack:
|
||||||
current_pack = self.settings.get('audio', 'sound_pack', 'default')
|
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}")
|
self.current_pack_label.setText(f"Current soundpack: {current_pack}")
|
||||||
|
|
||||||
def refresh_soundpacks(self):
|
def refresh_soundpacks(self):
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ class TimelineView(AccessibleTreeWidget):
|
|||||||
# Fetch timeline, notifications, followers/following, conversations, bookmarks, blocked/muted users
|
# Fetch timeline, notifications, followers/following, conversations, bookmarks, blocked/muted users
|
||||||
if self.timeline_type == "notifications":
|
if self.timeline_type == "notifications":
|
||||||
timeline_data = self.activitypub_client.get_notifications(limit=posts_per_page)
|
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":
|
elif self.timeline_type == "followers":
|
||||||
# Get current user account info first
|
# Get current user account info first
|
||||||
user_info = self.activitypub_client.verify_credentials()
|
user_info = self.activitypub_client.verify_credentials()
|
||||||
@@ -151,7 +152,7 @@ class TimelineView(AccessibleTreeWidget):
|
|||||||
"""Load real timeline data from ActivityPub API"""
|
"""Load real timeline data from ActivityPub API"""
|
||||||
# Check for new content by comparing newest post ID (only for regular timelines)
|
# Check for new content by comparing newest post ID (only for regular timelines)
|
||||||
has_new_content = False
|
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
|
# Check if the first post (newest) is different from what we had
|
||||||
current_newest_id = timeline_data[0]['id']
|
current_newest_id = timeline_data[0]['id']
|
||||||
if current_newest_id != self.newest_post_id:
|
if current_newest_id != self.newest_post_id:
|
||||||
@@ -569,6 +570,7 @@ class TimelineView(AccessibleTreeWidget):
|
|||||||
limit=posts_per_page,
|
limit=posts_per_page,
|
||||||
max_id=self.oldest_post_id
|
max_id=self.oldest_post_id
|
||||||
)
|
)
|
||||||
|
# No special case needed - notifications are handled by the main notifications case
|
||||||
elif self.timeline_type == "followers":
|
elif self.timeline_type == "followers":
|
||||||
user_info = self.activitypub_client.verify_credentials()
|
user_info = self.activitypub_client.verify_credentials()
|
||||||
more_data = self.activitypub_client.get_followers(
|
more_data = self.activitypub_client.get_followers(
|
||||||
@@ -1132,3 +1134,66 @@ class TimelineView(AccessibleTreeWidget):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error unmuting user: {e}")
|
print(f"Error unmuting user: {e}")
|
||||||
self.sound_manager.play_error()
|
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