import 'package:flutter/material.dart'; import '../../theme/app_theme.dart'; import '../sojorn_rich_text.dart'; import 'markdown_post_body.dart'; import 'post_view_mode.dart'; /// Post body text with reading-optimized typography. /// /// Design Intent: /// - Post body is the hero - clear visual hierarchy /// - Line height adjusts based on content length /// - Typography communicates emotional tone, not just content /// - Supports both plain text and Markdown formatting /// - ViewMode controls truncation behavior class PostBody extends StatelessWidget { final String text; final String? bodyFormat; // 'plain' or 'markdown' final String? backgroundId; // theme id final bool isReflective; final PostViewMode mode; final bool hideUrls; const PostBody({ super.key, required this.text, this.bodyFormat, this.backgroundId, this.isReflective = false, this.mode = PostViewMode.feed, this.hideUrls = false, }); /// Check if text contains Markdown syntax bool _hasMarkdownSyntax(String text) { // Check for common Markdown patterns return text.contains('**') || // Bold text.contains('_') || // Italic text.startsWith('#') || // Headers text.contains('[') && text.contains('](') || // Links text.contains('```') || // Code blocks text.contains('- ') || // Lists text.contains('> '); // Blockquotes } /// Determine max lines based on view mode int? get _maxLines { switch (mode) { case PostViewMode.feed: return 12; // Truncate in feed case PostViewMode.detail: return null; // Show all in detail case PostViewMode.compact: return 6; // More compact in profile lists case PostViewMode.thread: return 4; // Very compact in thread replies } } @override Widget build(BuildContext context) { // 1. Trim trailing whitespace // 2. Collapse 3+ newlines into 2 (max one empty line) final cleanedText = text .replaceAll(RegExp(r'\s+$'), '') .replaceAll(RegExp(r'\n{3,}'), '\n\n'); final isMarkdown = bodyFormat == 'markdown' || _hasMarkdownSyntax(cleanedText); final TextStyle style; if (isReflective) { style = AppTheme.postBodyReflective; } else { final estimatedLines = (cleanedText.length / 45).ceil(); if (estimatedLines <= 3) { style = AppTheme.postBodyShort; } else if (estimatedLines >= 10) { style = AppTheme.postBodyLong; } else { style = AppTheme.postBody; } } final int? maxLines = _maxLines; // If we have a maxLines limit, we want to show "Expand post..." if it's exceeded if (maxLines != null) { // Approximate line height (fontSize * 1.5 height + a bit of buffer) final double lineHeight = (style.fontSize ?? 17.0) * 1.6; final double maxHeight = maxLines * lineHeight; return LayoutBuilder( builder: (context, constraints) { final content = isMarkdown ? MarkdownPostBody( markdown: text, baseStyle: style, // We don't pass maxLines to MarkdownPostBody here because we handle clipping ourselves // to show the "Expand" button at the bottom correctly ) : sojornRichText( text: cleanedText, style: style, hideUrls: hideUrls, ); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ConstrainedBox( constraints: BoxConstraints(maxHeight: maxHeight), child: ClipRect( child: content, ), ), _ExpandIndicator(maxLines: maxLines, text: text), ], ); }, ); } if (isMarkdown) { return MarkdownPostBody( markdown: text, baseStyle: style, maxLines: maxLines, ); } return sojornRichText( text: cleanedText, style: style, maxLines: maxLines, hideUrls: hideUrls, ); } } class _ExpandIndicator extends StatelessWidget { final int maxLines; final String text; const _ExpandIndicator({required this.maxLines, required this.text}); @override Widget build(BuildContext context) { // Basic heuristic: check if line count is high or text is long final lineCount = '\n'.allMatches(text).length + 1; final isLong = text.length > 400 || lineCount > maxLines; if (!isLong) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.only(top: 8), child: Row( children: [ Text( 'Expand post...', style: TextStyle( color: AppTheme.brightNavy, fontWeight: FontWeight.bold, fontSize: 14, ), ), const SizedBox(width: 4), Icon(Icons.keyboard_arrow_down, size: 16, color: AppTheme.brightNavy), ], ), ); } }