···
1
+
import 'package:flutter/material.dart';
2
+
import 'package:provider/provider.dart';
4
+
import '../../constants/app_colors.dart';
5
+
import '../../models/comment.dart';
6
+
import '../../providers/auth_provider.dart';
7
+
import '../../widgets/comment_card.dart';
8
+
import '../../widgets/comment_thread.dart';
9
+
import '../../widgets/status_bar_overlay.dart';
10
+
import '../compose/reply_screen.dart';
12
+
/// Focused thread screen for viewing deep comment threads
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.
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
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.
28
+
class FocusedThreadScreen extends StatelessWidget {
29
+
const FocusedThreadScreen({
30
+
required this.thread,
31
+
required this.ancestors,
32
+
required this.onReply,
36
+
/// The comment thread to focus on (becomes the new root)
37
+
final ThreadViewComment thread;
39
+
/// Ancestor comments leading to this thread (for context display)
40
+
final List<ThreadViewComment> ancestors;
42
+
/// Callback when user replies to a comment
43
+
final Future<void> Function(String content, ThreadViewComment parent) onReply;
46
+
Widget build(BuildContext context) {
48
+
backgroundColor: AppColors.background,
49
+
body: _FocusedThreadBody(
51
+
ancestors: ancestors,
58
+
class _FocusedThreadBody extends StatefulWidget {
59
+
const _FocusedThreadBody({
60
+
required this.thread,
61
+
required this.ancestors,
62
+
required this.onReply,
65
+
final ThreadViewComment thread;
66
+
final List<ThreadViewComment> ancestors;
67
+
final Future<void> Function(String content, ThreadViewComment parent) onReply;
70
+
State<_FocusedThreadBody> createState() => _FocusedThreadBodyState();
73
+
class _FocusedThreadBodyState extends State<_FocusedThreadBody> {
74
+
final Set<String> _collapsedComments = {};
75
+
final ScrollController _scrollController = ScrollController();
76
+
final GlobalKey _anchorKey = GlobalKey();
81
+
// Scroll to anchor comment after build
82
+
WidgetsBinding.instance.addPostFrameCallback((_) {
89
+
_scrollController.dispose();
93
+
void _scrollToAnchor() {
94
+
final context = _anchorKey.currentContext;
95
+
if (context != null) {
96
+
Scrollable.ensureVisible(
98
+
duration: const Duration(milliseconds: 300),
99
+
curve: Curves.easeOut,
104
+
void _toggleCollapsed(String uri) {
106
+
if (_collapsedComments.contains(uri)) {
107
+
_collapsedComments.remove(uri);
109
+
_collapsedComments.add(uri);
114
+
void _openReplyScreen(ThreadViewComment comment) {
115
+
// Check authentication
116
+
final authProvider = context.read<AuthProvider>();
117
+
if (!authProvider.isAuthenticated) {
118
+
ScaffoldMessenger.of(context).showSnackBar(
120
+
content: Text('Sign in to reply'),
121
+
behavior: SnackBarBehavior.floating,
127
+
Navigator.of(context).push(
128
+
MaterialPageRoute<void>(
129
+
builder: (context) => ReplyScreen(
131
+
onSubmit: (content) => widget.onReply(content, comment),
137
+
/// Navigate deeper into a nested thread
138
+
void _onContinueThread(
139
+
ThreadViewComment thread,
140
+
List<ThreadViewComment> ancestors,
142
+
Navigator.of(context).push(
143
+
MaterialPageRoute<void>(
144
+
builder: (context) => FocusedThreadScreen(
146
+
ancestors: ancestors,
147
+
onReply: widget.onReply,
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;
162
+
controller: _scrollController,
165
+
const SliverAppBar(
166
+
backgroundColor: AppColors.background,
167
+
surfaceTintColor: Colors.transparent,
168
+
foregroundColor: AppColors.textPrimary,
173
+
fontWeight: FontWeight.w600,
176
+
centerTitle: false,
185
+
sliver: SliverList(
186
+
delegate: SliverChildListDelegate([
187
+
// Ancestor comments (shown flat, not nested)
188
+
...widget.ancestors.map(_buildAncestorComment),
190
+
// Anchor comment (the focused comment) - made prominent
193
+
child: _buildAnchorComment(),
196
+
// Replies (if any)
197
+
if (widget.thread.replies != null &&
198
+
widget.thread.replies!.isNotEmpty)
199
+
...widget.thread.replies!.map((reply) {
200
+
return CommentThread(
204
+
onCommentTap: _openReplyScreen,
205
+
collapsedComments: _collapsedComments,
206
+
onCollapseToggle: _toggleCollapsed,
207
+
onContinueThread: _onContinueThread,
208
+
ancestors: [widget.thread],
212
+
// Empty state if no replies
213
+
if (widget.thread.replies == null ||
214
+
widget.thread.replies!.isEmpty)
217
+
// Bottom padding to allow anchor to scroll to top
218
+
SizedBox(height: minBottomPadding),
225
+
// Prevents content showing through transparent status bar
226
+
const StatusBarOverlay(),
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) {
236
+
child: CommentCard(
237
+
comment: ancestor.comment,
238
+
onTap: () => _openReplyScreen(ancestor),
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
247
+
decoration: BoxDecoration(
248
+
// Subtle highlight to distinguish anchor from ancestors
249
+
color: AppColors.primary.withValues(alpha: 0.05),
252
+
color: AppColors.primary.withValues(alpha: 0.6),
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)
269
+
/// Build empty state when there are no replies
270
+
Widget _buildNoReplies() {
272
+
padding: const EdgeInsets.all(32),
273
+
alignment: Alignment.center,
277
+
Icons.chat_bubble_outline_rounded,
279
+
color: AppColors.textSecondary.withValues(alpha: 0.5),
281
+
const SizedBox(height: 16),
285
+
color: AppColors.textSecondary.withValues(alpha: 0.7),
289
+
const SizedBox(height: 8),
291
+
'Be the first to reply to this comment',
293
+
color: AppColors.textSecondary.withValues(alpha: 0.5),