···
import 'package:cached_network_image/cached_network_image.dart';
2
+
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
···
import '../../providers/auth_provider.dart';
import '../../providers/comments_provider.dart';
import '../../providers/vote_provider.dart';
14
+
import '../../services/comments_provider_cache.dart';
import '../../utils/community_handle_utils.dart';
import '../../utils/error_messages.dart';
import '../../widgets/comment_thread.dart';
···
final ScrollController _scrollController = ScrollController();
final GlobalKey _commentsHeaderKey = GlobalKey();
51
-
// Current sort option
52
-
String _currentSort = 'hot';
53
+
// Cached provider from CommentsProviderCache
54
+
late CommentsProvider _commentsProvider;
55
+
CommentsProviderCache? _commentsCache;
57
+
// Track initialization state
58
+
bool _isInitialized = false;
60
+
// Track if provider has been invalidated (e.g., by sign-out)
61
+
bool _providerInvalidated = false;
58
-
// Initialize scroll controller for pagination
_scrollController.addListener(_onScroll);
61
-
// Load comments after frame is built using provider from tree
68
+
// Initialize provider after frame is built
WidgetsBinding.instance.addPostFrameCallback((_) {
71
+
_initializeProvider();
72
+
_setupAuthListener();
77
+
/// Listen for auth state changes to handle sign-out
78
+
void _setupAuthListener() {
79
+
final authProvider = context.read<AuthProvider>();
80
+
authProvider.addListener(_onAuthChanged);
83
+
/// Handle auth state changes (specifically sign-out)
84
+
void _onAuthChanged() {
85
+
if (!mounted) return;
87
+
final authProvider = context.read<AuthProvider>();
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;
95
+
debugPrint('🚪 User signed out - cleaning up PostDetailScreen');
98
+
// Remove listener from provider (it's disposed but this is safe)
100
+
_commentsProvider.removeListener(_onProviderChanged);
102
+
// Provider already disposed - expected
105
+
// Navigate back to feed
107
+
Navigator.of(context).popUntil((route) => route.isFirst);
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,
122
+
// Listen for changes to trigger rebuilds
123
+
_commentsProvider.addListener(_onProviderChanged);
125
+
// Check if we already have cached data
126
+
if (_commentsProvider.comments.isNotEmpty) {
127
+
// Already have data - restore scroll position immediately
130
+
'📦 Using cached comments (${_commentsProvider.comments.length})',
133
+
_restoreScrollPosition();
135
+
// Background refresh if data is stale
136
+
if (_commentsProvider.isStale) {
138
+
debugPrint('🔄 Data stale, refreshing in background');
140
+
_commentsProvider.loadComments(refresh: true);
143
+
// No cached data - load fresh
144
+
_commentsProvider.loadComments(refresh: true);
148
+
_isInitialized = true;
154
+
// Remove auth listener
156
+
context.read<AuthProvider>().removeListener(_onAuthChanged);
158
+
// Context may not be valid during dispose
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) {
165
+
_commentsCache?.releaseProvider(widget.post.post.uri);
167
+
// Cache may already be disposed
171
+
// Remove provider listener if not already invalidated
172
+
if (_isInitialized && !_providerInvalidated) {
174
+
_commentsProvider.removeListener(_onProviderChanged);
176
+
// Provider may already be disposed
_scrollController.dispose();
75
-
/// Load comments for the current post
76
-
void _loadComments() {
77
-
context.read<CommentsProvider>().loadComments(
78
-
postUri: widget.post.post.uri,
79
-
postCid: widget.post.post.cid,
183
+
/// Handle provider changes
184
+
void _onProviderChanged() {
84
-
/// Handle sort changes from dropdown
85
-
Future<void> _onSortChanged(String newSort) async {
86
-
final previousSort = _currentSort;
190
+
/// Restore scroll position from provider
191
+
void _restoreScrollPosition() {
192
+
final savedPosition = _commentsProvider.scrollPosition;
193
+
if (savedPosition <= 0) {
197
+
WidgetsBinding.instance.addPostFrameCallback((_) {
198
+
if (!mounted || !_scrollController.hasClients) {
89
-
_currentSort = newSort;
202
+
final maxExtent = _scrollController.position.maxScrollExtent;
203
+
final targetPosition = savedPosition.clamp(0.0, maxExtent);
205
+
if (targetPosition > 0) {
206
+
_scrollController.jumpTo(targetPosition);
208
+
debugPrint('📍 Restored scroll to $targetPosition (max: $maxExtent)');
92
-
final commentsProvider = context.read<CommentsProvider>();
93
-
final success = await commentsProvider.setSortOption(newSort);
214
+
/// Handle sort changes from dropdown
215
+
Future<void> _onSortChanged(String newSort) async {
216
+
final success = await _commentsProvider.setSortOption(newSort);
95
-
// Show error snackbar and revert UI if sort change failed
218
+
// Show error snackbar if sort change failed
if (!success && mounted) {
98
-
_currentSort = previousSort;
ScaffoldMessenger.of(context).showSnackBar(
content: const Text('Failed to change sort order. Please try again.'),
···
/// Handle scroll for pagination
240
+
// Don't interact with disposed provider
241
+
if (_providerInvalidated) return;
243
+
// Save scroll position to provider on every scroll event
244
+
if (_scrollController.hasClients) {
245
+
_commentsProvider.saveScrollPosition(_scrollController.position.pixels);
248
+
// Load more comments when near bottom
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
123
-
context.read<CommentsProvider>().loadMoreComments();
251
+
_commentsProvider.loadMoreComments();
/// Handle pull-to-refresh
Future<void> _onRefresh() async {
129
-
final commentsProvider = context.read<CommentsProvider>();
130
-
await commentsProvider.refreshComments();
257
+
// Don't interact with disposed provider
258
+
if (_providerInvalidated) return;
260
+
await _commentsProvider.refreshComments();
Widget build(BuildContext context) {
136
-
backgroundColor: AppColors.background,
137
-
body: _buildContent(),
138
-
bottomNavigationBar: _buildActionBar(),
265
+
// Show loading until provider is initialized
266
+
if (!_isInitialized) {
267
+
return const Scaffold(
268
+
backgroundColor: AppColors.background,
269
+
body: FullScreenLoading(),
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(),
281
+
// Provide the cached CommentsProvider to descendant widgets
282
+
return ChangeNotifierProvider.value(
283
+
value: _commentsProvider,
285
+
backgroundColor: AppColors.background,
286
+
body: _buildContent(),
287
+
bottomNavigationBar: _buildActionBar(),
···
Navigator.of(context).push(
369
-
ReplyScreen(post: widget.post, onSubmit: _handleCommentSubmit),
518
+
(context) => ReplyScreen(
520
+
onSubmit: _handleCommentSubmit,
521
+
commentsProvider: _commentsProvider,
/// Handle comment submission (reply to post)
Future<void> _handleCommentSubmit(String content) async {
376
-
final commentsProvider = context.read<CommentsProvider>();
final messenger = ScaffoldMessenger.of(context);
380
-
await commentsProvider.createComment(content: content);
532
+
await _commentsProvider.createComment(content: content);
···
ThreadViewComment parentComment,
410
-
final commentsProvider = context.read<CommentsProvider>();
final messenger = ScaffoldMessenger.of(context);
414
-
await commentsProvider.createComment(
565
+
await _commentsProvider.createComment(
parentComment: parentComment,
···
(context) => ReplyScreen(
onSubmit: (content) => _handleCommentReply(content, comment),
614
+
commentsProvider: _commentsProvider,
···
Navigator.of(context).push(
475
-
builder: (context) => FocusedThreadScreen(
477
-
ancestors: ancestors,
478
-
onReply: _handleCommentReply,
628
+
(context) => FocusedThreadScreen(
630
+
ancestors: ancestors,
631
+
onReply: _handleCommentReply,
632
+
commentsProvider: _commentsProvider,
···
542
-
delegate: SliverChildBuilderDelegate(
544
-
// Post card (index 0)
548
-
// Reuse PostCard (hide comment button in
550
-
// Use ValueListenableBuilder to only rebuild
551
-
// when time changes
554
-
currentTimeNotifier:
555
-
commentsProvider.currentTimeNotifier,
696
+
delegate: SliverChildBuilderDelegate(
698
+
// Post card (index 0)
702
+
// Reuse PostCard (hide comment button in
704
+
// Use ValueListenableBuilder to only rebuild
705
+
// when time changes
708
+
currentTimeNotifier:
709
+
commentsProvider.currentTimeNotifier,
712
+
// Visual divider before comments section
714
+
margin: const EdgeInsets.symmetric(
718
+
color: AppColors.border,
558
-
// Visual divider before comments section
560
-
margin: const EdgeInsets.symmetric(vertical: 16),
562
-
color: AppColors.border,
721
+
// Comments header with sort dropdown
723
+
key: _commentsHeaderKey,
724
+
commentCount: comments.length,
725
+
currentSort: commentsProvider.sort,
726
+
onSortChanged: _onSortChanged,
565
-
// Comments header with sort dropdown
567
-
key: _commentsHeaderKey,
568
-
commentCount: comments.length,
569
-
currentSort: _currentSort,
570
-
onSortChanged: _onSortChanged,
732
+
// Loading indicator or error at the end
733
+
if (index == comments.length + 1) {
734
+
if (isLoadingMore) {
735
+
return const InlineLoading();
737
+
if (error != null) {
738
+
return InlineError(
739
+
message: ErrorMessages.getUserFriendly(error),
743
+
..loadMoreComments();
576
-
// Loading indicator or error at the end
577
-
if (index == comments.length + 1) {
578
-
if (isLoadingMore) {
579
-
return const InlineLoading();
581
-
if (error != null) {
582
-
return InlineError(
583
-
message: ErrorMessages.getUserFriendly(error),
587
-
..loadMoreComments();
749
+
// Comment item - use existing CommentThread widget
750
+
final comment = comments[index - 1];
751
+
return _CommentItem(
753
+
currentTimeNotifier:
754
+
commentsProvider.currentTimeNotifier,
755
+
onCommentTap: _openReplyToComment,
757
+
commentsProvider.collapsedComments,
758
+
onCollapseToggle: commentsProvider.toggleCollapsed,
759
+
onContinueThread: _onContinueThread,
593
-
// Comment item - use existing CommentThread widget
594
-
final comment = comments[index - 1];
595
-
return _CommentItem(
597
-
currentTimeNotifier:
598
-
commentsProvider.currentTimeNotifier,
599
-
onCommentTap: _openReplyToComment,
600
-
collapsedComments: commentsProvider.collapsedComments,
601
-
onCollapseToggle: commentsProvider.toggleCollapsed,
602
-
onContinueThread: _onContinueThread,
608
-
(isLoadingMore || error != null ? 1 : 0),
765
+
(isLoadingMore || error != null ? 1 : 0),
// Prevents content showing through transparent status bar
const StatusBarOverlay(),
···
final Set<String> collapsedComments;
final void Function(String uri)? onCollapseToggle;
final void Function(ThreadViewComment, List<ThreadViewComment>)?
Widget build(BuildContext context) {