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