1import 'package:flutter/material.dart'; 2import 'package:provider/provider.dart'; 3 4import '../../constants/app_colors.dart'; 5import '../../models/comment.dart'; 6import '../../providers/auth_provider.dart'; 7import '../../widgets/comment_card.dart'; 8import '../../widgets/comment_thread.dart'; 9import '../../widgets/status_bar_overlay.dart'; 10import '../compose/reply_screen.dart'; 11 12/// Focused thread screen for viewing deep comment threads 13/// 14/// Displays a specific comment as the "anchor" with its full reply tree. 15/// Used when user taps "Read X more replies" on a deeply nested thread. 16/// 17/// Shows: 18/// - Ancestor comments shown flat at the top (walking up the chain) 19/// - The anchor comment (highlighted as the focus) 20/// - All replies threaded below with fresh depth starting at 0 21/// 22/// ## Collapsed State 23/// This screen maintains its own collapsed comment state, intentionally 24/// providing a "fresh slate" experience. When the user navigates back, 25/// any collapsed state is reset. This is by design - it allows users to 26/// explore deep threads without their collapse choices persisting across 27/// navigation, keeping the focused view clean and predictable. 28class FocusedThreadScreen extends StatelessWidget { 29 const FocusedThreadScreen({ 30 required this.thread, 31 required this.ancestors, 32 required this.onReply, 33 super.key, 34 }); 35 36 /// The comment thread to focus on (becomes the new root) 37 final ThreadViewComment thread; 38 39 /// Ancestor comments leading to this thread (for context display) 40 final List<ThreadViewComment> ancestors; 41 42 /// Callback when user replies to a comment 43 final Future<void> Function(String content, ThreadViewComment parent) onReply; 44 45 @override 46 Widget build(BuildContext context) { 47 return Scaffold( 48 backgroundColor: AppColors.background, 49 body: _FocusedThreadBody( 50 thread: thread, 51 ancestors: ancestors, 52 onReply: onReply, 53 ), 54 ); 55 } 56} 57 58class _FocusedThreadBody extends StatefulWidget { 59 const _FocusedThreadBody({ 60 required this.thread, 61 required this.ancestors, 62 required this.onReply, 63 }); 64 65 final ThreadViewComment thread; 66 final List<ThreadViewComment> ancestors; 67 final Future<void> Function(String content, ThreadViewComment parent) onReply; 68 69 @override 70 State<_FocusedThreadBody> createState() => _FocusedThreadBodyState(); 71} 72 73class _FocusedThreadBodyState extends State<_FocusedThreadBody> { 74 final Set<String> _collapsedComments = {}; 75 final ScrollController _scrollController = ScrollController(); 76 final GlobalKey _anchorKey = GlobalKey(); 77 78 @override 79 void initState() { 80 super.initState(); 81 // Scroll to anchor comment after build 82 WidgetsBinding.instance.addPostFrameCallback((_) { 83 _scrollToAnchor(); 84 }); 85 } 86 87 @override 88 void dispose() { 89 _scrollController.dispose(); 90 super.dispose(); 91 } 92 93 void _scrollToAnchor() { 94 final context = _anchorKey.currentContext; 95 if (context != null) { 96 Scrollable.ensureVisible( 97 context, 98 duration: const Duration(milliseconds: 300), 99 curve: Curves.easeOut, 100 ); 101 } 102 } 103 104 void _toggleCollapsed(String uri) { 105 setState(() { 106 if (_collapsedComments.contains(uri)) { 107 _collapsedComments.remove(uri); 108 } else { 109 _collapsedComments.add(uri); 110 } 111 }); 112 } 113 114 void _openReplyScreen(ThreadViewComment comment) { 115 // Check authentication 116 final authProvider = context.read<AuthProvider>(); 117 if (!authProvider.isAuthenticated) { 118 ScaffoldMessenger.of(context).showSnackBar( 119 const SnackBar( 120 content: Text('Sign in to reply'), 121 behavior: SnackBarBehavior.floating, 122 ), 123 ); 124 return; 125 } 126 127 Navigator.of(context).push( 128 MaterialPageRoute<void>( 129 builder: (context) => ReplyScreen( 130 comment: comment, 131 onSubmit: (content) => widget.onReply(content, comment), 132 ), 133 ), 134 ); 135 } 136 137 /// Navigate deeper into a nested thread 138 void _onContinueThread( 139 ThreadViewComment thread, 140 List<ThreadViewComment> ancestors, 141 ) { 142 Navigator.of(context).push( 143 MaterialPageRoute<void>( 144 builder: (context) => FocusedThreadScreen( 145 thread: thread, 146 ancestors: ancestors, 147 onReply: widget.onReply, 148 ), 149 ), 150 ); 151 } 152 153 @override 154 Widget build(BuildContext context) { 155 // Calculate minimum bottom padding to allow anchor to scroll to top 156 final screenHeight = MediaQuery.of(context).size.height; 157 final minBottomPadding = screenHeight * 0.6; 158 159 return Stack( 160 children: [ 161 CustomScrollView( 162 controller: _scrollController, 163 slivers: [ 164 // App bar 165 const SliverAppBar( 166 backgroundColor: AppColors.background, 167 surfaceTintColor: Colors.transparent, 168 foregroundColor: AppColors.textPrimary, 169 title: Text( 170 'Thread', 171 style: TextStyle( 172 fontSize: 18, 173 fontWeight: FontWeight.w600, 174 ), 175 ), 176 centerTitle: false, 177 elevation: 0, 178 floating: true, 179 snap: true, 180 ), 181 182 // Content 183 SliverSafeArea( 184 top: false, 185 sliver: SliverList( 186 delegate: SliverChildListDelegate([ 187 // Ancestor comments (shown flat, not nested) 188 ...widget.ancestors.map(_buildAncestorComment), 189 190 // Anchor comment (the focused comment) - made prominent 191 KeyedSubtree( 192 key: _anchorKey, 193 child: _buildAnchorComment(), 194 ), 195 196 // Replies (if any) 197 if (widget.thread.replies != null && 198 widget.thread.replies!.isNotEmpty) 199 ...widget.thread.replies!.map((reply) { 200 return CommentThread( 201 thread: reply, 202 depth: 1, 203 maxDepth: 6, 204 onCommentTap: _openReplyScreen, 205 collapsedComments: _collapsedComments, 206 onCollapseToggle: _toggleCollapsed, 207 onContinueThread: _onContinueThread, 208 ancestors: [widget.thread], 209 ); 210 }), 211 212 // Empty state if no replies 213 if (widget.thread.replies == null || 214 widget.thread.replies!.isEmpty) 215 _buildNoReplies(), 216 217 // Bottom padding to allow anchor to scroll to top 218 SizedBox(height: minBottomPadding), 219 ]), 220 ), 221 ), 222 ], 223 ), 224 225 // Prevents content showing through transparent status bar 226 const StatusBarOverlay(), 227 ], 228 ); 229 } 230 231 /// Build an ancestor comment (shown flat as context above anchor) 232 /// Styled more subtly than the anchor to show it's contextual 233 Widget _buildAncestorComment(ThreadViewComment ancestor) { 234 return Opacity( 235 opacity: 0.6, 236 child: CommentCard( 237 comment: ancestor.comment, 238 onTap: () => _openReplyScreen(ancestor), 239 ), 240 ); 241 } 242 243 /// Build the anchor comment (the focused comment) with prominent styling 244 Widget _buildAnchorComment() { 245 // Note: CommentCard has its own Consumer<VoteProvider> for vote state 246 return Container( 247 decoration: BoxDecoration( 248 // Subtle highlight to distinguish anchor from ancestors 249 color: AppColors.primary.withValues(alpha: 0.05), 250 border: Border( 251 left: BorderSide( 252 color: AppColors.primary.withValues(alpha: 0.6), 253 width: 3, 254 ), 255 ), 256 ), 257 child: CommentCard( 258 comment: widget.thread.comment, 259 onTap: () => _openReplyScreen(widget.thread), 260 onLongPress: () => _toggleCollapsed(widget.thread.comment.uri), 261 isCollapsed: _collapsedComments.contains(widget.thread.comment.uri), 262 collapsedCount: _collapsedComments.contains(widget.thread.comment.uri) 263 ? CommentThread.countDescendants(widget.thread) 264 : 0, 265 ), 266 ); 267 } 268 269 /// Build empty state when there are no replies 270 Widget _buildNoReplies() { 271 return Container( 272 padding: const EdgeInsets.all(32), 273 alignment: Alignment.center, 274 child: Column( 275 children: [ 276 Icon( 277 Icons.chat_bubble_outline_rounded, 278 size: 48, 279 color: AppColors.textSecondary.withValues(alpha: 0.5), 280 ), 281 const SizedBox(height: 16), 282 Text( 283 'No replies yet', 284 style: TextStyle( 285 color: AppColors.textSecondary.withValues(alpha: 0.7), 286 fontSize: 15, 287 ), 288 ), 289 const SizedBox(height: 8), 290 Text( 291 'Be the first to reply to this comment', 292 style: TextStyle( 293 color: AppColors.textSecondary.withValues(alpha: 0.5), 294 fontSize: 13, 295 ), 296 ), 297 ], 298 ), 299 ); 300 } 301}