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_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}"
 | 
			
		||||
        
 | 
			
		||||
@@ -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}"
 | 
			
		||||
    
 | 
			
		||||
    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"""
 | 
			
		||||
        return {
 | 
			
		||||
@@ -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
 | 
			
		||||
        }
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user