Main coves client
1import 'package:cached_network_image/cached_network_image.dart';
2import 'package:flutter/foundation.dart';
3import 'package:flutter/material.dart';
4import 'package:flutter/services.dart';
5import 'package:provider/provider.dart';
6import 'package:share_plus/share_plus.dart';
7
8import '../../constants/app_colors.dart';
9import '../../models/comment.dart';
10import '../../models/post.dart';
11import '../../providers/auth_provider.dart';
12import '../../providers/comments_provider.dart';
13import '../../providers/vote_provider.dart';
14import '../../services/comments_provider_cache.dart';
15import '../../utils/community_handle_utils.dart';
16import '../../utils/error_messages.dart';
17import '../../widgets/comment_thread.dart';
18import '../../widgets/comments_header.dart';
19import '../../widgets/icons/share_icon.dart';
20import '../../widgets/loading_error_states.dart';
21import '../../widgets/post_action_bar.dart';
22import '../../widgets/post_card.dart';
23import '../../widgets/status_bar_overlay.dart';
24import '../compose/reply_screen.dart';
25import 'focused_thread_screen.dart';
26
27/// Post Detail Screen
28///
29/// Displays a full post with its comments.
30/// Architecture: Standalone screen for route destination and PageView child.
31///
32/// Features:
33/// - Full post display (reuses PostCard widget)
34/// - Sort selector (Hot/Top/New) using dropdown
35/// - Comment list with ListView.builder for performance
36/// - Pull-to-refresh with RefreshIndicator
37/// - Loading, empty, and error states
38/// - Automatic comment loading on screen init
39class PostDetailScreen extends StatefulWidget {
40 const PostDetailScreen({required this.post, super.key});
41
42 /// Post to display (passed via route extras)
43 final FeedViewPost post;
44
45 @override
46 State<PostDetailScreen> createState() => _PostDetailScreenState();
47}
48
49class _PostDetailScreenState extends State<PostDetailScreen> {
50 final ScrollController _scrollController = ScrollController();
51 final GlobalKey _commentsHeaderKey = GlobalKey();
52
53 // Cached provider from CommentsProviderCache
54 late CommentsProvider _commentsProvider;
55 CommentsProviderCache? _commentsCache;
56
57 // Track initialization state
58 bool _isInitialized = false;
59
60 // Track if provider has been invalidated (e.g., by sign-out)
61 bool _providerInvalidated = false;
62
63 @override
64 void initState() {
65 super.initState();
66 _scrollController.addListener(_onScroll);
67
68 // Initialize provider after frame is built
69 WidgetsBinding.instance.addPostFrameCallback((_) {
70 if (mounted) {
71 _initializeProvider();
72 _setupAuthListener();
73 }
74 });
75 }
76
77 /// Listen for auth state changes to handle sign-out
78 void _setupAuthListener() {
79 final authProvider = context.read<AuthProvider>();
80 authProvider.addListener(_onAuthChanged);
81 }
82
83 /// Handle auth state changes (specifically sign-out)
84 void _onAuthChanged() {
85 if (!mounted) return;
86
87 final authProvider = context.read<AuthProvider>();
88
89 // If user signed out while viewing this screen, navigate back
90 // The CommentsProviderCache has already disposed our provider
91 if (!authProvider.isAuthenticated && _isInitialized && !_providerInvalidated) {
92 _providerInvalidated = true;
93
94 if (kDebugMode) {
95 debugPrint('🚪 User signed out - cleaning up PostDetailScreen');
96 }
97
98 // Remove listener from provider (it's disposed but this is safe)
99 try {
100 _commentsProvider.removeListener(_onProviderChanged);
101 } on Exception {
102 // Provider already disposed - expected
103 }
104
105 // Navigate back to feed
106 if (mounted) {
107 Navigator.of(context).popUntil((route) => route.isFirst);
108 }
109 }
110 }
111
112 /// Initialize provider from cache and restore state
113 void _initializeProvider() {
114 // Get or create provider from cache
115 final cache = context.read<CommentsProviderCache>();
116 _commentsCache = cache;
117 _commentsProvider = cache.acquireProvider(
118 postUri: widget.post.post.uri,
119 postCid: widget.post.post.cid,
120 );
121
122 // Listen for changes to trigger rebuilds
123 _commentsProvider.addListener(_onProviderChanged);
124
125 // Check if we already have cached data
126 if (_commentsProvider.comments.isNotEmpty) {
127 // Already have data - restore scroll position immediately
128 if (kDebugMode) {
129 debugPrint(
130 '📦 Using cached comments (${_commentsProvider.comments.length})',
131 );
132 }
133 _restoreScrollPosition();
134
135 // Background refresh if data is stale
136 if (_commentsProvider.isStale) {
137 if (kDebugMode) {
138 debugPrint('🔄 Data stale, refreshing in background');
139 }
140 _commentsProvider.loadComments(refresh: true);
141 }
142 } else {
143 // No cached data - load fresh
144 _commentsProvider.loadComments(refresh: true);
145 }
146
147 setState(() {
148 _isInitialized = true;
149 });
150 }
151
152 @override
153 void dispose() {
154 // Remove auth listener
155 try {
156 context.read<AuthProvider>().removeListener(_onAuthChanged);
157 } on Exception {
158 // Context may not be valid during dispose
159 }
160
161 // Release provider pin in cache (prevents LRU eviction disposing an active
162 // provider while this screen is in the navigation stack).
163 if (_isInitialized) {
164 try {
165 _commentsCache?.releaseProvider(widget.post.post.uri);
166 } on Exception {
167 // Cache may already be disposed
168 }
169 }
170
171 // Remove provider listener if not already invalidated
172 if (_isInitialized && !_providerInvalidated) {
173 try {
174 _commentsProvider.removeListener(_onProviderChanged);
175 } on Exception {
176 // Provider may already be disposed
177 }
178 }
179 _scrollController.dispose();
180 super.dispose();
181 }
182
183 /// Handle provider changes
184 void _onProviderChanged() {
185 if (mounted) {
186 setState(() {});
187 }
188 }
189
190 /// Restore scroll position from provider
191 void _restoreScrollPosition() {
192 final savedPosition = _commentsProvider.scrollPosition;
193 if (savedPosition <= 0) {
194 return;
195 }
196
197 WidgetsBinding.instance.addPostFrameCallback((_) {
198 if (!mounted || !_scrollController.hasClients) {
199 return;
200 }
201
202 final maxExtent = _scrollController.position.maxScrollExtent;
203 final targetPosition = savedPosition.clamp(0.0, maxExtent);
204
205 if (targetPosition > 0) {
206 _scrollController.jumpTo(targetPosition);
207 if (kDebugMode) {
208 debugPrint('📍 Restored scroll to $targetPosition (max: $maxExtent)');
209 }
210 }
211 });
212 }
213
214 /// Handle sort changes from dropdown
215 Future<void> _onSortChanged(String newSort) async {
216 final success = await _commentsProvider.setSortOption(newSort);
217
218 // Show error snackbar if sort change failed
219 if (!success && mounted) {
220 ScaffoldMessenger.of(context).showSnackBar(
221 SnackBar(
222 content: const Text('Failed to change sort order. Please try again.'),
223 backgroundColor: AppColors.primary,
224 behavior: SnackBarBehavior.floating,
225 duration: const Duration(seconds: 3),
226 action: SnackBarAction(
227 label: 'Retry',
228 textColor: AppColors.textPrimary,
229 onPressed: () {
230 _onSortChanged(newSort);
231 },
232 ),
233 ),
234 );
235 }
236 }
237
238 /// Handle scroll for pagination
239 void _onScroll() {
240 // Don't interact with disposed provider
241 if (_providerInvalidated) return;
242
243 // Save scroll position to provider on every scroll event
244 if (_scrollController.hasClients) {
245 _commentsProvider.saveScrollPosition(_scrollController.position.pixels);
246 }
247
248 // Load more comments when near bottom
249 if (_scrollController.position.pixels >=
250 _scrollController.position.maxScrollExtent - 200) {
251 _commentsProvider.loadMoreComments();
252 }
253 }
254
255 /// Handle pull-to-refresh
256 Future<void> _onRefresh() async {
257 // Don't interact with disposed provider
258 if (_providerInvalidated) return;
259
260 await _commentsProvider.refreshComments();
261 }
262
263 @override
264 Widget build(BuildContext context) {
265 // Show loading until provider is initialized
266 if (!_isInitialized) {
267 return const Scaffold(
268 backgroundColor: AppColors.background,
269 body: FullScreenLoading(),
270 );
271 }
272
273 // If provider was invalidated (sign-out), show loading while navigating away
274 if (_providerInvalidated) {
275 return const Scaffold(
276 backgroundColor: AppColors.background,
277 body: FullScreenLoading(),
278 );
279 }
280
281 // Provide the cached CommentsProvider to descendant widgets
282 return ChangeNotifierProvider.value(
283 value: _commentsProvider,
284 child: Scaffold(
285 backgroundColor: AppColors.background,
286 body: _buildContent(),
287 bottomNavigationBar: _buildActionBar(),
288 ),
289 );
290 }
291
292 /// Build community title with avatar and handle
293 Widget _buildCommunityTitle() {
294 final community = widget.post.post.community;
295 final displayHandle = CommunityHandleUtils.formatHandleForDisplay(
296 community.handle,
297 );
298
299 return Row(
300 mainAxisSize: MainAxisSize.min,
301 children: [
302 // Community avatar
303 if (community.avatar != null && community.avatar!.isNotEmpty)
304 ClipRRect(
305 borderRadius: BorderRadius.circular(16),
306 child: CachedNetworkImage(
307 imageUrl: community.avatar!,
308 width: 32,
309 height: 32,
310 fit: BoxFit.cover,
311 placeholder: (context, url) => _buildFallbackAvatar(community),
312 errorWidget:
313 (context, url, error) => _buildFallbackAvatar(community),
314 ),
315 )
316 else
317 _buildFallbackAvatar(community),
318 const SizedBox(width: 8),
319 // Community handle with styled parts
320 if (displayHandle != null)
321 Flexible(child: _buildStyledHandle(displayHandle))
322 else
323 Flexible(
324 child: Text(
325 community.name,
326 style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
327 overflow: TextOverflow.ellipsis,
328 ),
329 ),
330 ],
331 );
332 }
333
334 /// Build styled community handle with color-coded parts
335 Widget _buildStyledHandle(String displayHandle) {
336 // Format: !gaming@coves.social
337 final atIndex = displayHandle.indexOf('@');
338 final communityPart = displayHandle.substring(0, atIndex);
339 final instancePart = displayHandle.substring(atIndex);
340
341 return Text.rich(
342 TextSpan(
343 children: [
344 TextSpan(
345 text: communityPart,
346 style: const TextStyle(
347 color: AppColors.communityName,
348 fontSize: 16,
349 fontWeight: FontWeight.w600,
350 ),
351 ),
352 TextSpan(
353 text: instancePart,
354 style: TextStyle(
355 color: AppColors.textSecondary.withValues(alpha: 0.8),
356 fontSize: 16,
357 fontWeight: FontWeight.w600,
358 ),
359 ),
360 ],
361 ),
362 overflow: TextOverflow.ellipsis,
363 );
364 }
365
366 /// Build fallback avatar with first letter
367 Widget _buildFallbackAvatar(CommunityRef community) {
368 final firstLetter = community.name.isNotEmpty ? community.name[0] : '?';
369 return Container(
370 width: 32,
371 height: 32,
372 decoration: BoxDecoration(
373 color: AppColors.primary,
374 borderRadius: BorderRadius.circular(16),
375 ),
376 child: Center(
377 child: Text(
378 firstLetter.toUpperCase(),
379 style: const TextStyle(
380 color: AppColors.textPrimary,
381 fontSize: 14,
382 fontWeight: FontWeight.bold,
383 ),
384 ),
385 ),
386 );
387 }
388
389 /// Handle share button tap
390 Future<void> _handleShare() async {
391 // Add haptic feedback
392 await HapticFeedback.lightImpact();
393
394 // TODO: Generate proper deep link URL when deep linking is implemented
395 final postUri = widget.post.post.uri;
396 final title = widget.post.post.title ?? 'Check out this post';
397
398 await Share.share('$title\n\n$postUri', subject: title);
399 }
400
401 /// Build bottom action bar with vote, save, and comment actions
402 Widget _buildActionBar() {
403 return Consumer<VoteProvider>(
404 builder: (context, voteProvider, child) {
405 final isVoted = voteProvider.isLiked(widget.post.post.uri);
406 final adjustedScore = voteProvider.getAdjustedScore(
407 widget.post.post.uri,
408 widget.post.post.stats.score,
409 );
410
411 // Create a modified post with adjusted score for display
412 final displayPost = FeedViewPost(
413 post: PostView(
414 uri: widget.post.post.uri,
415 cid: widget.post.post.cid,
416 rkey: widget.post.post.rkey,
417 author: widget.post.post.author,
418 community: widget.post.post.community,
419 createdAt: widget.post.post.createdAt,
420 indexedAt: widget.post.post.indexedAt,
421 text: widget.post.post.text,
422 title: widget.post.post.title,
423 stats: PostStats(
424 upvotes: widget.post.post.stats.upvotes,
425 downvotes: widget.post.post.stats.downvotes,
426 score: adjustedScore,
427 commentCount: widget.post.post.stats.commentCount,
428 ),
429 embed: widget.post.post.embed,
430 facets: widget.post.post.facets,
431 ),
432 reason: widget.post.reason,
433 );
434
435 return PostActionBar(
436 post: displayPost,
437 isVoted: isVoted,
438 onCommentInputTap: _openCommentComposer,
439 onCommentCountTap: _scrollToComments,
440 onVoteTap: () async {
441 // Check authentication
442 final authProvider = context.read<AuthProvider>();
443 if (!authProvider.isAuthenticated) {
444 ScaffoldMessenger.of(context).showSnackBar(
445 const SnackBar(
446 content: Text('Sign in to vote on posts'),
447 behavior: SnackBarBehavior.floating,
448 ),
449 );
450 return;
451 }
452
453 // Capture messenger before async operations
454 final messenger = ScaffoldMessenger.of(context);
455
456 // Light haptic feedback on both like and unlike
457 await HapticFeedback.lightImpact();
458 try {
459 await voteProvider.toggleVote(
460 postUri: widget.post.post.uri,
461 postCid: widget.post.post.cid,
462 );
463 } on Exception catch (e) {
464 if (mounted) {
465 messenger.showSnackBar(
466 SnackBar(
467 content: Text('Failed to vote: $e'),
468 behavior: SnackBarBehavior.floating,
469 ),
470 );
471 }
472 }
473 },
474 onSaveTap: () {
475 // TODO: Add save functionality
476 ScaffoldMessenger.of(context).showSnackBar(
477 const SnackBar(
478 content: Text('Save feature coming soon!'),
479 behavior: SnackBarBehavior.floating,
480 ),
481 );
482 },
483 );
484 },
485 );
486 }
487
488 /// Scroll to the comments section
489 void _scrollToComments() {
490 final context = _commentsHeaderKey.currentContext;
491 if (context != null) {
492 Scrollable.ensureVisible(
493 context,
494 duration: const Duration(milliseconds: 300),
495 curve: Curves.easeInOut,
496 );
497 }
498 }
499
500 /// Open the reply screen for composing a comment
501 void _openCommentComposer() {
502 // Check authentication
503 final authProvider = context.read<AuthProvider>();
504 if (!authProvider.isAuthenticated) {
505 ScaffoldMessenger.of(context).showSnackBar(
506 const SnackBar(
507 content: Text('Sign in to comment'),
508 behavior: SnackBarBehavior.floating,
509 ),
510 );
511 return;
512 }
513
514 // Navigate to reply screen with full post context
515 Navigator.of(context).push(
516 MaterialPageRoute<void>(
517 builder:
518 (context) => ReplyScreen(
519 post: widget.post,
520 onSubmit: _handleCommentSubmit,
521 commentsProvider: _commentsProvider,
522 ),
523 ),
524 );
525 }
526
527 /// Handle comment submission (reply to post)
528 Future<void> _handleCommentSubmit(String content) async {
529 final messenger = ScaffoldMessenger.of(context);
530
531 try {
532 await _commentsProvider.createComment(content: content);
533
534 if (mounted) {
535 messenger.showSnackBar(
536 const SnackBar(
537 content: Text('Comment posted'),
538 behavior: SnackBarBehavior.floating,
539 duration: Duration(seconds: 2),
540 ),
541 );
542 }
543 } on Exception catch (e) {
544 if (mounted) {
545 messenger.showSnackBar(
546 SnackBar(
547 content: Text('Failed to post comment: $e'),
548 behavior: SnackBarBehavior.floating,
549 backgroundColor: AppColors.primary,
550 ),
551 );
552 }
553 rethrow; // Let ReplyScreen know submission failed
554 }
555 }
556
557 /// Handle reply to a comment (nested reply)
558 Future<void> _handleCommentReply(
559 String content,
560 ThreadViewComment parentComment,
561 ) async {
562 final messenger = ScaffoldMessenger.of(context);
563
564 try {
565 await _commentsProvider.createComment(
566 content: content,
567 parentComment: parentComment,
568 );
569
570 if (mounted) {
571 messenger.showSnackBar(
572 const SnackBar(
573 content: Text('Reply posted'),
574 behavior: SnackBarBehavior.floating,
575 duration: Duration(seconds: 2),
576 ),
577 );
578 }
579 } on Exception catch (e) {
580 if (mounted) {
581 messenger.showSnackBar(
582 SnackBar(
583 content: Text('Failed to post reply: $e'),
584 behavior: SnackBarBehavior.floating,
585 backgroundColor: AppColors.primary,
586 ),
587 );
588 }
589 rethrow; // Let ReplyScreen know submission failed
590 }
591 }
592
593 /// Open reply screen for replying to a comment
594 void _openReplyToComment(ThreadViewComment comment) {
595 // Check authentication
596 final authProvider = context.read<AuthProvider>();
597 if (!authProvider.isAuthenticated) {
598 ScaffoldMessenger.of(context).showSnackBar(
599 const SnackBar(
600 content: Text('Sign in to reply'),
601 behavior: SnackBarBehavior.floating,
602 ),
603 );
604 return;
605 }
606
607 // Navigate to reply screen with comment context
608 Navigator.of(context).push(
609 MaterialPageRoute<void>(
610 builder:
611 (context) => ReplyScreen(
612 comment: comment,
613 onSubmit: (content) => _handleCommentReply(content, comment),
614 commentsProvider: _commentsProvider,
615 ),
616 ),
617 );
618 }
619
620 /// Navigate to focused thread screen for deep threads
621 void _onContinueThread(
622 ThreadViewComment thread,
623 List<ThreadViewComment> ancestors,
624 ) {
625 Navigator.of(context).push(
626 MaterialPageRoute<void>(
627 builder:
628 (context) => FocusedThreadScreen(
629 thread: thread,
630 ancestors: ancestors,
631 onReply: _handleCommentReply,
632 commentsProvider: _commentsProvider,
633 ),
634 ),
635 );
636 }
637
638 /// Build main content area
639 Widget _buildContent() {
640 // Use Consumer to rebuild when comments provider changes
641 return Consumer<CommentsProvider>(
642 builder: (context, commentsProvider, child) {
643 final isLoading = commentsProvider.isLoading;
644 final error = commentsProvider.error;
645 final comments = commentsProvider.comments;
646 final isLoadingMore = commentsProvider.isLoadingMore;
647
648 // Loading state (only show full-screen loader for initial load)
649 if (isLoading && comments.isEmpty) {
650 return const FullScreenLoading();
651 }
652
653 // Error state (only show full-screen error when no comments loaded yet)
654 if (error != null && comments.isEmpty) {
655 return FullScreenError(
656 title: 'Failed to load comments',
657 message: ErrorMessages.getUserFriendly(error),
658 onRetry: commentsProvider.retry,
659 );
660 }
661
662 // Content with RefreshIndicator and floating SliverAppBar
663 // Wrapped in Stack to add solid status bar background overlay
664 return Stack(
665 children: [
666 RefreshIndicator(
667 onRefresh: _onRefresh,
668 color: AppColors.primary,
669 child: CustomScrollView(
670 controller: _scrollController,
671 slivers: [
672 // Floating app bar that hides on scroll down,
673 // shows on scroll up
674 SliverAppBar(
675 backgroundColor: AppColors.background,
676 surfaceTintColor: Colors.transparent,
677 foregroundColor: AppColors.textPrimary,
678 title: _buildCommunityTitle(),
679 centerTitle: false,
680 elevation: 0,
681 floating: true,
682 snap: true,
683 actions: [
684 IconButton(
685 icon: const ShareIcon(color: AppColors.textPrimary),
686 onPressed: _handleShare,
687 tooltip: 'Share',
688 ),
689 ],
690 ),
691
692 // Post + comments + loading indicator
693 SliverSafeArea(
694 top: false,
695 sliver: SliverList(
696 delegate: SliverChildBuilderDelegate(
697 (context, index) {
698 // Post card (index 0)
699 if (index == 0) {
700 return Column(
701 children: [
702 // Reuse PostCard (hide comment button in
703 // detail view)
704 // Use ValueListenableBuilder to only rebuild
705 // when time changes
706 _PostHeader(
707 post: widget.post,
708 currentTimeNotifier:
709 commentsProvider.currentTimeNotifier,
710 ),
711
712 // Visual divider before comments section
713 Container(
714 margin: const EdgeInsets.symmetric(
715 vertical: 16,
716 ),
717 height: 1,
718 color: AppColors.border,
719 ),
720
721 // Comments header with sort dropdown
722 CommentsHeader(
723 key: _commentsHeaderKey,
724 commentCount: comments.length,
725 currentSort: commentsProvider.sort,
726 onSortChanged: _onSortChanged,
727 ),
728 ],
729 );
730 }
731
732 // Loading indicator or error at the end
733 if (index == comments.length + 1) {
734 if (isLoadingMore) {
735 return const InlineLoading();
736 }
737 if (error != null) {
738 return InlineError(
739 message: ErrorMessages.getUserFriendly(error),
740 onRetry: () {
741 commentsProvider
742 ..clearError()
743 ..loadMoreComments();
744 },
745 );
746 }
747 }
748
749 // Comment item - use existing CommentThread widget
750 final comment = comments[index - 1];
751 return _CommentItem(
752 comment: comment,
753 currentTimeNotifier:
754 commentsProvider.currentTimeNotifier,
755 onCommentTap: _openReplyToComment,
756 collapsedComments:
757 commentsProvider.collapsedComments,
758 onCollapseToggle: commentsProvider.toggleCollapsed,
759 onContinueThread: _onContinueThread,
760 );
761 },
762 childCount:
763 1 +
764 comments.length +
765 (isLoadingMore || error != null ? 1 : 0),
766 ),
767 ),
768 ),
769 ],
770 ),
771 ),
772 // Prevents content showing through transparent status bar
773 const StatusBarOverlay(),
774 ],
775 );
776 },
777 );
778 }
779}
780
781/// Post header widget that only rebuilds when time changes
782///
783/// Extracted to prevent unnecessary rebuilds when comment list changes.
784/// Uses ValueListenableBuilder to listen only to time updates.
785class _PostHeader extends StatelessWidget {
786 const _PostHeader({required this.post, required this.currentTimeNotifier});
787
788 final FeedViewPost post;
789 final ValueNotifier<DateTime?> currentTimeNotifier;
790
791 @override
792 Widget build(BuildContext context) {
793 return ValueListenableBuilder<DateTime?>(
794 valueListenable: currentTimeNotifier,
795 builder: (context, currentTime, child) {
796 return PostCard(
797 post: post,
798 currentTime: currentTime,
799 showCommentButton: false,
800 disableNavigation: true,
801 showActions: false,
802 showHeader: false,
803 showBorder: false,
804 showFullText: true,
805 showAuthorFooter: true,
806 textFontSize: 16,
807 textLineHeight: 1.6,
808 embedHeight: 280,
809 titleFontSize: 20,
810 titleFontWeight: FontWeight.w600,
811 );
812 },
813 );
814 }
815}
816
817/// Comment item wrapper that only rebuilds when time changes
818///
819/// Uses ValueListenableBuilder to prevent rebuilds when unrelated
820/// provider state changes (like loading state or error state).
821class _CommentItem extends StatelessWidget {
822 const _CommentItem({
823 required this.comment,
824 required this.currentTimeNotifier,
825 this.onCommentTap,
826 this.collapsedComments = const {},
827 this.onCollapseToggle,
828 this.onContinueThread,
829 });
830
831 final ThreadViewComment comment;
832 final ValueNotifier<DateTime?> currentTimeNotifier;
833 final void Function(ThreadViewComment)? onCommentTap;
834 final Set<String> collapsedComments;
835 final void Function(String uri)? onCollapseToggle;
836 final void Function(ThreadViewComment, List<ThreadViewComment>)?
837 onContinueThread;
838
839 @override
840 Widget build(BuildContext context) {
841 return ValueListenableBuilder<DateTime?>(
842 valueListenable: currentTimeNotifier,
843 builder: (context, currentTime, child) {
844 return CommentThread(
845 thread: comment,
846 currentTime: currentTime,
847 maxDepth: 6,
848 onCommentTap: onCommentTap,
849 collapsedComments: collapsedComments,
850 onCollapseToggle: onCollapseToggle,
851 onContinueThread: onContinueThread,
852 );
853 },
854 );
855 }
856}