From 2b8382e4eb45d69b92db218ef13a47603bca3ea9 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 24 Jul 2025 01:45:32 -0400 Subject: [PATCH] Add fediverse client identification to posts and timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add application field to Post model to capture client metadata from API - Display client info in timeline view after timestamp (e.g. "5 minutes ago via Bifrost") - Show client information in post details dialog metadata section - Gracefully handle missing client data by silently omitting when unavailable - Handle common generic client names (Web, Mobile) with cleaner formatting - Support client identification for both original posts and reblogs 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/models/post.py | 37 ++++++++++++++++++++++++++---- src/widgets/post_details_dialog.py | 5 ++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/models/post.py b/src/models/post.py index 5cc6112..65f73dd 100644 --- a/src/models/post.py +++ b/src/models/post.py @@ -83,6 +83,9 @@ class Post: notification_type: Optional[str] = None # mention, reblog, favourite, follow, etc. notification_account: Optional[str] = None # account that triggered notification + # Application metadata (client that posted this) + application: Optional[Dict[str, Any]] = None # {'name': 'ClientName', 'website': 'url'} + def __post_init__(self): if self.media_attachments is None: self.media_attachments = [] @@ -174,7 +177,8 @@ class Post: emoji_reactions=data.get('emoji_reactions', []), expires_at=datetime.fromisoformat(data['expires_at'].replace('Z', '+00:00')) if data.get('expires_at') else None, local=data.get('local', True), - thread_muted=data.get('thread_muted', False) + thread_muted=data.get('thread_muted', False), + application=data.get('application') ) return post @@ -268,9 +272,14 @@ class Post: content = self.get_display_content() relative_time = self.get_relative_time() - # Include timestamp if available - if relative_time: + # Include timestamp and client info if available + client_info = self.get_client_info() + if relative_time and client_info: + summary = f"{author}: {content} ({relative_time} {client_info})" + elif relative_time: summary = f"{author}: {content} ({relative_time})" + elif client_info: + summary = f"{author}: {content} ({client_info})" else: summary = f"{author}: {content}" @@ -361,6 +370,25 @@ class Post: status_text = ", press Enter to vote" return f"Poll: {options_count} options, {vote_text}, multiple {choice_text} {'allowed' if multiple else 'not allowed'}{status_text}{expiry_text}" + + def get_client_info(self) -> str: + """Get client application information for display""" + # For reblogs, show the client of the original post + target_post = self.reblog if self.reblog else self + + if target_post.application and target_post.application.get('name'): + client_name = target_post.application['name'] + # Handle common generic names for better user experience + if client_name.lower() in ['web', 'mastodon web']: + return "via Web" + elif client_name.lower() in ['mobile', 'mastodon mobile']: + return "via Mobile" + else: + return f"via {client_name}" + + # If no application data, silently omit (don't show generic fallback) + # This gracefully handles cases where servers don't provide application info + return "" def to_api_data(self) -> Dict[str, Any]: """Convert Post back to API response format""" @@ -397,5 +425,6 @@ class Post: 'emoji_reactions': self.emoji_reactions, 'expires_at': self.expires_at.isoformat() if self.expires_at else None, 'local': self.local, - 'thread_muted': self.thread_muted + 'thread_muted': self.thread_muted, + 'application': self.application } \ No newline at end of file diff --git a/src/widgets/post_details_dialog.py b/src/widgets/post_details_dialog.py index a1ce505..e03265e 100644 --- a/src/widgets/post_details_dialog.py +++ b/src/widgets/post_details_dialog.py @@ -394,6 +394,11 @@ class PostDetailsDialog(QDialog): if hasattr(self.post, "language") and self.post.language: metadata_parts.append(f"Language: {self.post.language}") + # Add client information if available + client_info = self.post.get_client_info() + if client_info: + metadata_parts.append(f"Client: {client_info.replace('via ', '')}") + if metadata_parts: content_parts.append("Post Details:") content_parts.extend(metadata_parts)