Main coves client
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}