···
import 'package:cached_network_image/cached_network_image.dart';
+
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';
+
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();
+
// Cached provider from CommentsProviderCache
+
late CommentsProvider _commentsProvider;
+
CommentsProviderCache? _commentsCache;
+
// Track initialization state
+
bool _isInitialized = false;
+
// Track if provider has been invalidated (e.g., by sign-out)
+
bool _providerInvalidated = false;
_scrollController.addListener(_onScroll);
+
// Initialize provider after frame is built
WidgetsBinding.instance.addPostFrameCallback((_) {
+
/// Listen for auth state changes to handle sign-out
+
void _setupAuthListener() {
+
final authProvider = context.read<AuthProvider>();
+
authProvider.addListener(_onAuthChanged);
+
/// Handle auth state changes (specifically sign-out)
+
void _onAuthChanged() {
+
final authProvider = context.read<AuthProvider>();
+
// If user signed out while viewing this screen, navigate back
+
// The CommentsProviderCache has already disposed our provider
+
if (!authProvider.isAuthenticated && _isInitialized && !_providerInvalidated) {
+
_providerInvalidated = true;
+
debugPrint('🚪 User signed out - cleaning up PostDetailScreen');
+
// Remove listener from provider (it's disposed but this is safe)
+
_commentsProvider.removeListener(_onProviderChanged);
+
// Provider already disposed - expected
+
// Navigate back to feed
+
Navigator.of(context).popUntil((route) => route.isFirst);
+
/// Initialize provider from cache and restore state
+
void _initializeProvider() {
+
// Get or create provider from cache
+
final cache = context.read<CommentsProviderCache>();
+
_commentsCache = cache;
+
_commentsProvider = cache.acquireProvider(
+
postUri: widget.post.post.uri,
+
postCid: widget.post.post.cid,
+
// Listen for changes to trigger rebuilds
+
_commentsProvider.addListener(_onProviderChanged);
+
// Check if we already have cached data
+
if (_commentsProvider.comments.isNotEmpty) {
+
// Already have data - restore scroll position immediately
+
'📦 Using cached comments (${_commentsProvider.comments.length})',
+
_restoreScrollPosition();
+
// Background refresh if data is stale
+
if (_commentsProvider.isStale) {
+
debugPrint('🔄 Data stale, refreshing in background');
+
_commentsProvider.loadComments(refresh: true);
+
// No cached data - load fresh
+
_commentsProvider.loadComments(refresh: true);
+
// Remove auth listener
+
context.read<AuthProvider>().removeListener(_onAuthChanged);
+
// Context may not be valid during dispose
+
// Release provider pin in cache (prevents LRU eviction disposing an active
+
// provider while this screen is in the navigation stack).
+
_commentsCache?.releaseProvider(widget.post.post.uri);
+
// Cache may already be disposed
+
// Remove provider listener if not already invalidated
+
if (_isInitialized && !_providerInvalidated) {
+
_commentsProvider.removeListener(_onProviderChanged);
+
// Provider may already be disposed
_scrollController.dispose();
+
/// Handle provider changes
+
void _onProviderChanged() {
+
/// Restore scroll position from provider
+
void _restoreScrollPosition() {
+
final savedPosition = _commentsProvider.scrollPosition;
+
if (savedPosition <= 0) {
+
WidgetsBinding.instance.addPostFrameCallback((_) {
+
if (!mounted || !_scrollController.hasClients) {
+
final maxExtent = _scrollController.position.maxScrollExtent;
+
final targetPosition = savedPosition.clamp(0.0, maxExtent);
+
if (targetPosition > 0) {
+
_scrollController.jumpTo(targetPosition);
+
debugPrint('📍 Restored scroll to $targetPosition (max: $maxExtent)');
+
/// Handle sort changes from dropdown
+
Future<void> _onSortChanged(String newSort) async {
+
final success = await _commentsProvider.setSortOption(newSort);
+
// Show error snackbar if sort change failed
if (!success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
content: const Text('Failed to change sort order. Please try again.'),
···
/// Handle scroll for pagination
+
// Don't interact with disposed provider
+
if (_providerInvalidated) return;
+
// Save scroll position to provider on every scroll event
+
if (_scrollController.hasClients) {
+
_commentsProvider.saveScrollPosition(_scrollController.position.pixels);
+
// Load more comments when near bottom
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
+
_commentsProvider.loadMoreComments();
/// Handle pull-to-refresh
Future<void> _onRefresh() async {
+
// Don't interact with disposed provider
+
if (_providerInvalidated) return;
+
await _commentsProvider.refreshComments();
Widget build(BuildContext context) {
+
// Show loading until provider is initialized
+
backgroundColor: AppColors.background,
+
body: FullScreenLoading(),
+
// If provider was invalidated (sign-out), show loading while navigating away
+
if (_providerInvalidated) {
+
backgroundColor: AppColors.background,
+
body: FullScreenLoading(),
+
// Provide the cached CommentsProvider to descendant widgets
+
return ChangeNotifierProvider.value(
+
value: _commentsProvider,
+
backgroundColor: AppColors.background,
+
bottomNavigationBar: _buildActionBar(),
···
Navigator.of(context).push(
+
(context) => ReplyScreen(
+
onSubmit: _handleCommentSubmit,
+
commentsProvider: _commentsProvider,
/// Handle comment submission (reply to post)
Future<void> _handleCommentSubmit(String content) async {
final messenger = ScaffoldMessenger.of(context);
+
await _commentsProvider.createComment(content: content);
···
ThreadViewComment parentComment,
final messenger = ScaffoldMessenger.of(context);
+
await _commentsProvider.createComment(
parentComment: parentComment,
···
(context) => ReplyScreen(
onSubmit: (content) => _handleCommentReply(content, comment),
+
commentsProvider: _commentsProvider,
···
Navigator.of(context).push(
+
(context) => FocusedThreadScreen(
+
onReply: _handleCommentReply,
+
commentsProvider: _commentsProvider,
···
+
delegate: SliverChildBuilderDelegate(
+
// Reuse PostCard (hide comment button in
+
// Use ValueListenableBuilder to only rebuild
+
commentsProvider.currentTimeNotifier,
+
// Visual divider before comments section
+
margin: const EdgeInsets.symmetric(
+
color: AppColors.border,
+
// Comments header with sort dropdown
+
key: _commentsHeaderKey,
+
commentCount: comments.length,
+
currentSort: commentsProvider.sort,
+
onSortChanged: _onSortChanged,
+
// Loading indicator or error at the end
+
if (index == comments.length + 1) {
+
return const InlineLoading();
+
message: ErrorMessages.getUserFriendly(error),
+
// Comment item - use existing CommentThread widget
+
final comment = comments[index - 1];
+
commentsProvider.currentTimeNotifier,
+
onCommentTap: _openReplyToComment,
+
commentsProvider.collapsedComments,
+
onCollapseToggle: commentsProvider.toggleCollapsed,
+
onContinueThread: _onContinueThread,
+
(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) {