Add fediverse client identification to posts and timeline

- 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 <noreply@anthropic.com>
This commit is contained in:
Storm Dragon
2025-07-24 01:45:32 -04:00
parent 2365b6a134
commit 2b8382e4eb
2 changed files with 38 additions and 4 deletions

View File

@@ -83,6 +83,9 @@ class Post:
notification_type: Optional[str] = None # mention, reblog, favourite, follow, etc. notification_type: Optional[str] = None # mention, reblog, favourite, follow, etc.
notification_account: Optional[str] = None # account that triggered notification 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): def __post_init__(self):
if self.media_attachments is None: if self.media_attachments is None:
self.media_attachments = [] self.media_attachments = []
@@ -174,7 +177,8 @@ class Post:
emoji_reactions=data.get('emoji_reactions', []), emoji_reactions=data.get('emoji_reactions', []),
expires_at=datetime.fromisoformat(data['expires_at'].replace('Z', '+00:00')) if data.get('expires_at') else None, expires_at=datetime.fromisoformat(data['expires_at'].replace('Z', '+00:00')) if data.get('expires_at') else None,
local=data.get('local', True), 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 return post
@@ -268,9 +272,14 @@ class Post:
content = self.get_display_content() content = self.get_display_content()
relative_time = self.get_relative_time() relative_time = self.get_relative_time()
# Include timestamp if available # Include timestamp and client info if available
if relative_time: 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})" summary = f"{author}: {content} ({relative_time})"
elif client_info:
summary = f"{author}: {content} ({client_info})"
else: else:
summary = f"{author}: {content}" summary = f"{author}: {content}"
@@ -362,6 +371,25 @@ class Post:
return f"Poll: {options_count} options, {vote_text}, multiple {choice_text} {'allowed' if multiple else 'not allowed'}{status_text}{expiry_text}" 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]: def to_api_data(self) -> Dict[str, Any]:
"""Convert Post back to API response format""" """Convert Post back to API response format"""
return { return {
@@ -397,5 +425,6 @@ class Post:
'emoji_reactions': self.emoji_reactions, 'emoji_reactions': self.emoji_reactions,
'expires_at': self.expires_at.isoformat() if self.expires_at else None, 'expires_at': self.expires_at.isoformat() if self.expires_at else None,
'local': self.local, 'local': self.local,
'thread_muted': self.thread_muted 'thread_muted': self.thread_muted,
'application': self.application
} }

View File

@@ -394,6 +394,11 @@ class PostDetailsDialog(QDialog):
if hasattr(self.post, "language") and self.post.language: if hasattr(self.post, "language") and self.post.language:
metadata_parts.append(f"Language: {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: if metadata_parts:
content_parts.append("Post Details:") content_parts.append("Post Details:")
content_parts.extend(metadata_parts) content_parts.extend(metadata_parts)