import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:timeago/timeago.dart' as timeago; import '../models/post.dart'; import '../models/thread_node.dart'; import '../theme/app_theme.dart'; import '../widgets/media/signed_media_image.dart'; import '../widgets/post/post_actions.dart'; /// Recursive widget for rendering threaded conversations (Reddit-style) class ThreadedCommentWidget extends StatefulWidget { final ThreadNode node; final VoidCallback? onReply; final VoidCallback? onLike; final Function(ThreadNode)? onNavigateToParent; final Function(ThreadNode)? onNavigateToChild; final bool isRootPost; const ThreadedCommentWidget({ super.key, required this.node, this.onReply, this.onLike, this.onNavigateToParent, this.onNavigateToChild, this.isRootPost = false, }); @override State createState() => _ThreadedCommentWidgetState(); } class _ThreadedCommentWidgetState extends State with TickerProviderStateMixin { bool _isExpanded = true; bool _isHovering = false; bool _showReplyBox = false; final _replyController = TextEditingController(); late AnimationController _expandController; late AnimationController _hoverController; late AnimationController _replyBoxController; late Animation _expandAnimation; late Animation _hoverAnimation; late Animation _replyBoxAnimation; @override void initState() { super.initState(); // Expand/collapse animation _expandController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _expandAnimation = CurvedAnimation( parent: _expandController, curve: Curves.easeInOut, ); _expandController.forward(); // Hover animation _hoverController = AnimationController( duration: const Duration(milliseconds: 200), vsync: this, ); _hoverAnimation = CurvedAnimation( parent: _hoverController, curve: Curves.easeInOut, ); // Reply box animation _replyBoxController = AnimationController( duration: const Duration(milliseconds: 250), vsync: this, ); _replyBoxAnimation = CurvedAnimation( parent: _replyBoxController, curve: Curves.easeInOut, ); } @override void dispose() { _expandController.dispose(); _hoverController.dispose(); _replyBoxController.dispose(); _replyController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // The post content with indentation _buildPostWithIndentation(), // Thread line connector if (widget.node.hasChildren && _isExpanded) _buildThreadLine(), // Recursive children with animation if (widget.node.hasChildren) _buildChildren(), // Inline reply box _buildInlineReplyBox(), ], ), ); } Widget _buildPostWithIndentation() { return AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, margin: EdgeInsets.only( left: widget.isRootPost ? 0 : (widget.node.depth * 16.0 + 8.0), right: 16, top: widget.isRootPost ? 0 : 8, bottom: 8, ), decoration: BoxDecoration( border: widget.isRootPost ? null : Border( left: BorderSide( color: AppTheme.navyBlue.withValues(alpha: _isHovering ? 0.6 : 0.2), width: _isHovering ? 3 : 2, ), ), borderRadius: BorderRadius.circular(8), color: _isHovering ? AppTheme.navyBlue.withValues(alpha: 0.05) : null, ), child: MouseRegion( onEnter: (_) { setState(() => _isHovering = true); _hoverController.forward(); }, onExit: (_) { setState(() => _isHovering = false); _hoverController.reverse(); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Chain navigation header if (!widget.isRootPost) _buildChainNavigation(), // Post content _buildPostContent(), ], ), ), ); } Widget _buildChainNavigation() { final totalReplies = widget.node.totalDescendants; final hasParent = widget.node.parent != null; return Container( margin: const EdgeInsets.only(bottom: 8), child: Row( children: [ // Navigate up chain if (hasParent) AnimatedContainer( duration: const Duration(milliseconds: 200), child: GestureDetector( onTap: () { if (widget.onNavigateToParent != null && widget.node.parent != null) { widget.onNavigateToParent!(widget.node.parent!); } }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: AppTheme.navyBlue.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.arrow_upward, size: 14, color: AppTheme.navyBlue), const SizedBox(width: 4), Text('Parent', style: TextStyle(fontSize: 12, color: AppTheme.navyBlue)), ], ), ), ), ), const Spacer(), // Chain navigation buttons Row( mainAxisSize: MainAxisSize.min, children: [ // Reply count with expand/collapse if (widget.node.hasChildren) GestureDetector( onTap: () { setState(() { _isExpanded = !_isExpanded; if (_isExpanded) { _expandController.forward(); } else { _expandController.reverse(); } }); }, child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: AppTheme.egyptianBlue.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ AnimatedSwitcher( duration: const Duration(milliseconds: 200), child: Icon( _isExpanded ? Icons.expand_less : Icons.expand_more, size: 14, color: AppTheme.egyptianBlue, key: ValueKey(_isExpanded), ), ), const SizedBox(width: 4), Text( '$totalReplies ${totalReplies == 1 ? "reply" : "replies"}', style: TextStyle(fontSize: 12, color: AppTheme.egyptianBlue), ), ], ), ), ), // Quick navigation to first child if (widget.node.hasChildren && widget.node.children.isNotEmpty) const SizedBox(width: 8), if (widget.node.hasChildren && widget.node.children.isNotEmpty) GestureDetector( onTap: () { if (widget.onNavigateToChild != null) { widget.onNavigateToChild!(widget.node.children.first); } }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), decoration: BoxDecoration( color: AppTheme.brightNavy.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( Icons.arrow_downward, size: 12, color: AppTheme.brightNavy, ), ), ), ], ), child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: AppTheme.egyptianBlue.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ AnimatedSwitcher( duration: const Duration(milliseconds: 200), child: Icon( _isExpanded ? Icons.expand_less : Icons.expand_more, size: 14, color: AppTheme.egyptianBlue, key: ValueKey(_isExpanded), ), ), const SizedBox(width: 4), Text( '$totalReplies ${totalReplies == 1 ? "reply" : "replies"}', style: TextStyle(fontSize: 12, color: AppTheme.egyptianBlue), ), ], ), ), ), ], ), ); } Widget _buildPostContent() { return Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Author info and metadata _buildAuthorRow(), const SizedBox(height: 8), // Post body _buildPostBody(), // Media if present if (widget.node.post.imageUrl != null) ...[ const SizedBox(height: 8), _buildPostImage(), ], const SizedBox(height: 8), // Actions bar _buildActionsBar(), ], ), ); } Widget _buildPostBody() { return Text( widget.node.post.body, style: GoogleFonts.inter( fontSize: 14, color: AppTheme.navyText, height: 1.4, ), ); } Widget _buildPostImage() { return ClipRRect( borderRadius: BorderRadius.circular(8), child: SignedMediaImage( url: widget.node.post.imageUrl!, width: double.infinity, height: 200, fit: BoxFit.cover, ), ); } Widget _buildActionsBar() { return PostActions( post: widget.node.post, onChain: _toggleReplyBox, ); } Widget _buildInlineReplyBox() { return SizeTransition( sizeFactor: _replyBoxAnimation, child: FadeTransition( opacity: _replyBoxAnimation, child: Container( margin: EdgeInsets.only( left: widget.isRootPost ? 0 : (widget.node.depth * 16.0 + 8.0), right: 16, top: 8, bottom: 16, ), decoration: BoxDecoration( color: AppTheme.navyBlue.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(12), border: Border.all( color: AppTheme.navyBlue.withValues(alpha: 0.2), width: 1, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Reply header Padding( padding: const EdgeInsets.all(12), child: Row( children: [ Icon(Icons.reply, size: 16, color: AppTheme.navyBlue), const SizedBox(width: 8), Text( 'Replying to ${widget.node.post.author?.displayName ?? 'Anonymous'}', style: GoogleFonts.inter( fontSize: 12, color: AppTheme.navyBlue, fontWeight: FontWeight.w500, ), ), const Spacer(), GestureDetector( onTap: _toggleReplyBox, child: Icon(Icons.close, size: 16, color: AppTheme.navyBlue), ), ], ), ), // Text input Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: TextField( controller: _replyController, maxLines: 3, minLines: 1, style: GoogleFonts.inter( fontSize: 14, color: AppTheme.navyText, ), decoration: InputDecoration( hintText: 'Write your reply...', hintStyle: GoogleFonts.inter( fontSize: 14, color: AppTheme.textSecondary, ), border: InputBorder.none, contentPadding: EdgeInsets.zero, ), ), ), // Action buttons Padding( padding: const EdgeInsets.all(12), child: Row( children: [ // Image button (placeholder for now) IconButton( onPressed: () { // TODO: Add image functionality }, icon: Icon(Icons.image, size: 18, color: AppTheme.textSecondary), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), const SizedBox(width: 16), // Character count Text( '${_replyController.text.length}/500', style: GoogleFonts.inter( fontSize: 12, color: AppTheme.textSecondary, ), ), const Spacer(), // Post button ElevatedButton( onPressed: _replyController.text.trim().isNotEmpty ? _submitReply : null, style: ElevatedButton.styleFrom( backgroundColor: AppTheme.brightNavy, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), ), child: Text( 'Reply', style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w600, ), ), ), ], ), ), ], ), ), ), ); } void _toggleReplyBox() { setState(() { _showReplyBox = !_showReplyBox; if (_showReplyBox) { _replyBoxController.forward(); _replyController.clear(); FocusScope.of(context).requestFocus(FocusNode()); } else { _replyBoxController.reverse(); } }); } void _submitReply() async { if (_replyController.text.trim().isEmpty) return; // TODO: Implement actual reply submission // For now, just close the box and clear the text setState(() { _showReplyBox = false; _replyBoxController.reverse(); _replyController.clear(); }); // Call the original onReply callback if provided if (widget.onReply != null) { widget.onReply!(); } } Widget _buildAuthorRow() { return Row( children: [ // Avatar Container( width: 32, height: 32, decoration: BoxDecoration( color: AppTheme.brightNavy.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Center( child: Text( 'L${widget.node.depth}', style: GoogleFonts.inter( color: AppTheme.brightNavy, fontSize: 10, fontWeight: FontWeight.w600, ), ), ), ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.node.post.author?.displayName ?? 'Anonymous', style: GoogleFonts.inter( color: AppTheme.textPrimary, fontSize: 14, fontWeight: FontWeight.w600, ), ), Text( timeago.format(widget.node.post.createdAt), style: GoogleFonts.inter( color: AppTheme.textSecondary, fontSize: 12, ), ), ], ), ), ], ); } Widget _buildThreadLine() { return Container( height: 20, margin: EdgeInsets.only( left: widget.isRootPost ? 0 : (widget.node.depth * 16.0 + 8.0 + 16), ), child: Container( width: 2, color: AppTheme.navyBlue.withValues(alpha: 0.3), ), ); } Widget _buildChildren() { return SizeTransition( sizeFactor: _expandAnimation, child: FadeTransition( opacity: _expandAnimation, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: widget.node.children.map((child) => ThreadedCommentWidget( node: child, onReply: widget.onReply, onLike: widget.onLike, onNavigateToParent: widget.onNavigateToParent, onNavigateToChild: widget.onNavigateToChild, ), ).toList(), ), ), ); } }