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:
@@ -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
|
||||||
}
|
}
|
@@ -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)
|
||||||
|
Reference in New Issue
Block a user