927 lines
27 KiB
Dart
927 lines
27 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:share_plus/share_plus.dart';
|
|
import 'dart:async';
|
|
import '../models/post_models.dart';
|
|
import '../services/post_service.dart';
|
|
import '../utils/invitation_utils.dart';
|
|
import '../screens/pages/post_view_page.dart';
|
|
|
|
class PostsList extends StatefulWidget {
|
|
final Future<Map<String, dynamic>> Function({bool forceRefresh}) fetchPosts;
|
|
final String emptyStateTitle;
|
|
final String emptyStateSubtitle;
|
|
final bool showRefreshIndicator;
|
|
final Widget? floatingActionButton;
|
|
|
|
const PostsList({
|
|
Key? key,
|
|
required this.fetchPosts,
|
|
this.emptyStateTitle = 'Nothing here..',
|
|
this.emptyStateSubtitle = 'Create the first post!',
|
|
this.showRefreshIndicator = true,
|
|
this.floatingActionButton,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
PostsListState createState() => PostsListState();
|
|
}
|
|
|
|
class PostsListState extends State<PostsList> {
|
|
bool _isLoading = true;
|
|
List<Post> _posts = [];
|
|
String _errorMessage = '';
|
|
StreamSubscription<List<Post>>? _postsStreamSubscription;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_setupPostsStream(); // This will also call _loadPosts()
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_postsStreamSubscription?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
void _setupPostsStream() {
|
|
// Only set up stream for main feed posts, not user posts
|
|
print('Setting up posts stream for main feed');
|
|
_postsStreamSubscription?.cancel(); // Cancel any existing subscription
|
|
|
|
_postsStreamSubscription = PostService.getPostsStream().listen(
|
|
(updatedPosts) {
|
|
print('📱 Posts stream received data: ${updatedPosts.length} posts');
|
|
if (mounted) {
|
|
// Update the posts directly instead of fetching again
|
|
setState(() {
|
|
_posts = updatedPosts;
|
|
_isLoading = false;
|
|
_errorMessage = '';
|
|
});
|
|
print(
|
|
'🎯 Posts UI updated with ${updatedPosts.length} posts - setState called',
|
|
);
|
|
} else {
|
|
print('⚠️ Widget not mounted, skipping UI update');
|
|
}
|
|
},
|
|
onError: (error) {
|
|
print('Posts stream error: $error');
|
|
if (mounted) {
|
|
setState(() {
|
|
_errorMessage = 'Stream error: $error';
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
// Also trigger an initial load to populate the stream
|
|
_loadPosts();
|
|
|
|
// Trigger stream with any existing cached data
|
|
Future.delayed(Duration(milliseconds: 100), () {
|
|
if (mounted) {
|
|
PostService.notifyStreamWithCurrentData();
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> _loadPosts({bool forceRefresh = false}) async {
|
|
// Don't show loading for cached data unless forcing refresh
|
|
final isInitialLoad = _posts.isEmpty;
|
|
|
|
if (isInitialLoad || forceRefresh) {
|
|
setState(() {
|
|
_isLoading = true;
|
|
_errorMessage = '';
|
|
});
|
|
}
|
|
|
|
try {
|
|
final result = await widget.fetchPosts(forceRefresh: forceRefresh);
|
|
if (mounted) {
|
|
setState(() {
|
|
if (result['success']) {
|
|
_posts = result['posts'];
|
|
_errorMessage = '';
|
|
} else {
|
|
_errorMessage = result['message'] ?? 'Failed to load posts';
|
|
}
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_errorMessage = 'Network error: $e';
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _refreshPosts() async {
|
|
await _loadPosts(forceRefresh: true);
|
|
|
|
if (widget.showRefreshIndicator) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Posts refreshed!'),
|
|
backgroundColor: Color(0xFF6A4C93),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
Widget body = _buildBody();
|
|
|
|
if (widget.showRefreshIndicator) {
|
|
body = RefreshIndicator(onRefresh: _refreshPosts, child: body);
|
|
}
|
|
|
|
return body;
|
|
}
|
|
|
|
Widget _buildBody() {
|
|
if (_isLoading) {
|
|
return Center(
|
|
child: CircularProgressIndicator(
|
|
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF6A4C93)),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (_errorMessage.isNotEmpty) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.error_outline, size: 64, color: Colors.grey[400]),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
_errorMessage,
|
|
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
SizedBox(height: 16),
|
|
ElevatedButton(
|
|
onPressed: _loadPosts,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Color(0xFF6A4C93),
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: Text('Try Again'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
if (_posts.isEmpty) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.post_add, size: 64, color: Colors.grey[400]),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
widget.emptyStateTitle,
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
SizedBox(height: 8),
|
|
Text(
|
|
widget.emptyStateSubtitle,
|
|
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return ListView.builder(
|
|
padding: EdgeInsets.symmetric(vertical: 8),
|
|
itemCount: _posts.length,
|
|
itemBuilder: (context, index) {
|
|
return PostCard(post: _posts[index]);
|
|
},
|
|
);
|
|
}
|
|
|
|
void refreshPosts() {
|
|
_loadPosts(forceRefresh: true);
|
|
}
|
|
}
|
|
|
|
class PostImageViewer extends StatelessWidget {
|
|
final String imageUrl;
|
|
|
|
const PostImageViewer({Key? key, required this.imageUrl}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: Colors.black,
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.transparent,
|
|
elevation: 0,
|
|
iconTheme: IconThemeData(color: Colors.white),
|
|
),
|
|
body: Center(
|
|
child: GestureDetector(
|
|
onTap: () => Navigator.of(context).pop(),
|
|
child: InteractiveViewer(
|
|
child: Image.network(
|
|
imageUrl,
|
|
fit: BoxFit.contain,
|
|
loadingBuilder: (context, child, loadingProgress) {
|
|
if (loadingProgress == null) return child;
|
|
return Center(
|
|
child: CircularProgressIndicator(
|
|
value: loadingProgress.expectedTotalBytes != null
|
|
? loadingProgress.cumulativeBytesLoaded /
|
|
loadingProgress.expectedTotalBytes!
|
|
: null,
|
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
|
),
|
|
);
|
|
},
|
|
errorBuilder: (context, error, stackTrace) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.broken_image, size: 64, color: Colors.white54),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
'Failed to load image',
|
|
style: TextStyle(color: Colors.white70, fontSize: 16),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class PostImagesCarousel extends StatelessWidget {
|
|
final List<String> images;
|
|
final Function(String imageUrl)? onImageTap;
|
|
|
|
const PostImagesCarousel({Key? key, required this.images, this.onImageTap})
|
|
: super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (images.isEmpty) return SizedBox.shrink();
|
|
|
|
return Container(
|
|
height: 200,
|
|
child: ListView.builder(
|
|
scrollDirection: Axis.horizontal,
|
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
|
itemCount: images.length,
|
|
itemBuilder: (context, index) {
|
|
return Container(
|
|
width: 280,
|
|
margin: EdgeInsets.only(right: index < images.length - 1 ? 12 : 0),
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
if (onImageTap != null) {
|
|
onImageTap!(images[index]);
|
|
} else {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) =>
|
|
PostImageViewer(imageUrl: images[index]),
|
|
),
|
|
);
|
|
}
|
|
},
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Image.network(
|
|
images[index],
|
|
fit: BoxFit.cover,
|
|
width: 280,
|
|
height: 200,
|
|
loadingBuilder: (context, child, loadingProgress) {
|
|
if (loadingProgress == null) return child;
|
|
return Container(
|
|
width: 280,
|
|
height: 200,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[200],
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Center(
|
|
child: CircularProgressIndicator(
|
|
value: loadingProgress.expectedTotalBytes != null
|
|
? loadingProgress.cumulativeBytesLoaded /
|
|
loadingProgress.expectedTotalBytes!
|
|
: null,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
Color(0xFF6A4C93),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
errorBuilder: (context, error, stackTrace) {
|
|
print('Image load error: $error');
|
|
print('Stack trace: $stackTrace');
|
|
print('Image URL: ${images[index]}');
|
|
return Container(
|
|
width: 280,
|
|
height: 200,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[200],
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.broken_image,
|
|
size: 48,
|
|
color: Colors.grey[400],
|
|
),
|
|
SizedBox(height: 8),
|
|
Text(
|
|
'Failed to load image',
|
|
style: TextStyle(
|
|
color: Colors.grey[600],
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class UserAvatar extends StatelessWidget {
|
|
final String displayName;
|
|
final String? avatarUrl;
|
|
final double radius;
|
|
final Color? backgroundColor;
|
|
|
|
const UserAvatar({
|
|
Key? key,
|
|
required this.displayName,
|
|
this.avatarUrl,
|
|
this.radius = 20,
|
|
this.backgroundColor,
|
|
}) : super(key: key);
|
|
|
|
Color _getAvatarColor(String displayName) {
|
|
final colors = [
|
|
Color(0xFF32B0A5),
|
|
Color(0xFF4600B9),
|
|
Color(0xFF6A4C93),
|
|
Color(0xFFFF6347),
|
|
Color(0xFF32CD32),
|
|
Color(0xFF9932CC),
|
|
];
|
|
|
|
int hash = displayName.hashCode;
|
|
return colors[hash.abs() % colors.length];
|
|
}
|
|
|
|
String _getAvatarLetter(String displayName) {
|
|
return displayName.isNotEmpty ? displayName[0].toUpperCase() : '?';
|
|
}
|
|
|
|
bool _isValidImageUrl(String? url) {
|
|
return url != null && url.isNotEmpty && url.trim().isNotEmpty;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final color = backgroundColor ?? _getAvatarColor(displayName);
|
|
final letter = _getAvatarLetter(displayName);
|
|
|
|
return CircleAvatar(
|
|
radius: radius,
|
|
backgroundColor: color,
|
|
child: _isValidImageUrl(avatarUrl)
|
|
? ClipOval(
|
|
child: Image.network(
|
|
avatarUrl!,
|
|
width: radius * 2,
|
|
height: radius * 2,
|
|
fit: BoxFit.cover,
|
|
loadingBuilder: (context, child, loadingProgress) {
|
|
if (loadingProgress == null) return child;
|
|
return SizedBox(
|
|
width: radius * 2,
|
|
height: radius * 2,
|
|
child: Center(
|
|
child: SizedBox(
|
|
width: radius * 0.6,
|
|
height: radius * 0.6,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
errorBuilder: (context, error, stackTrace) {
|
|
return Text(
|
|
letter,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: radius * 0.8,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
)
|
|
: Text(
|
|
letter,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: radius * 0.8,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class PostCard extends StatefulWidget {
|
|
final Post post;
|
|
|
|
const PostCard({Key? key, required this.post}) : super(key: key);
|
|
|
|
@override
|
|
_PostCardState createState() => _PostCardState();
|
|
}
|
|
|
|
class _PostCardState extends State<PostCard> {
|
|
late Post _currentPost;
|
|
bool _isLiking = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_currentPost = widget.post;
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(PostCard oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
// Force update current post to reflect any changes
|
|
if (oldWidget.post.id != widget.post.id ||
|
|
oldWidget.post.likes != widget.post.likes ||
|
|
oldWidget.post.liked != widget.post.liked ||
|
|
oldWidget.post.comments != widget.post.comments) {
|
|
setState(() {
|
|
_currentPost = widget.post;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _sharePost(Post post) {
|
|
final shareText =
|
|
'${post.creator.displayName} posted on Wesal.online:\n\n${post.body}';
|
|
Share.share(shareText);
|
|
}
|
|
|
|
Future<void> _toggleLike() async {
|
|
if (_isLiking) return; // Prevent multiple simultaneous requests
|
|
|
|
setState(() {
|
|
_isLiking = true;
|
|
// Optimistic update - immediately change UI
|
|
_currentPost = _currentPost.copyWith(
|
|
liked: !_currentPost.liked,
|
|
likes: _currentPost.liked
|
|
? _currentPost.likes - 1
|
|
: _currentPost.likes + 1,
|
|
);
|
|
});
|
|
|
|
try {
|
|
Map<String, dynamic> result;
|
|
if (widget.post.liked) {
|
|
result = await PostService.unlikePost(widget.post.id);
|
|
} else {
|
|
result = await PostService.likePost(widget.post.id);
|
|
}
|
|
|
|
if (result['success'] && result['post'] != null) {
|
|
// Update with server response
|
|
setState(() {
|
|
_currentPost = result['post'];
|
|
_isLiking = false;
|
|
});
|
|
} else {
|
|
// Revert optimistic update on failure
|
|
setState(() {
|
|
_currentPost = widget.post;
|
|
_isLiking = false;
|
|
});
|
|
|
|
// Show error message
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(result['message'] ?? 'Failed to update like'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Revert optimistic update on error
|
|
setState(() {
|
|
_currentPost = widget.post;
|
|
_isLiking = false;
|
|
});
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Network error occurred'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final creator = _currentPost.creator;
|
|
final relativeTime = InvitationUtils.getRelativeTime(
|
|
_currentPost.creationDate,
|
|
);
|
|
|
|
return GestureDetector(
|
|
onTap: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => PostViewPage(postId: _currentPost.id),
|
|
),
|
|
);
|
|
},
|
|
child: Container(
|
|
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Padding(
|
|
padding: EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
UserAvatar(
|
|
displayName: creator.displayName,
|
|
avatarUrl: creator.avatar,
|
|
radius: 20,
|
|
),
|
|
SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
creator.displayName,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 16,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
SizedBox(height: 2),
|
|
Text(
|
|
relativeTime,
|
|
style: TextStyle(
|
|
color: Colors.grey[600],
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 12),
|
|
Text(
|
|
_currentPost.body,
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
height: 1.4,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
if (_currentPost.images != null &&
|
|
_currentPost.images!.isNotEmpty) ...[
|
|
SizedBox(height: 12),
|
|
PostImagesCarousel(images: _currentPost.images!),
|
|
],
|
|
SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
IconButton(
|
|
onPressed: _toggleLike,
|
|
icon: Icon(
|
|
_currentPost.liked
|
|
? Icons.favorite
|
|
: Icons.favorite_border,
|
|
color: _currentPost.liked ? Colors.red : Colors.grey[600],
|
|
size: 24,
|
|
),
|
|
),
|
|
Text(
|
|
'${_currentPost.likes}',
|
|
style: TextStyle(
|
|
color: Colors.grey[700],
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
SizedBox(width: 16),
|
|
IconButton(
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) =>
|
|
PostViewPage(postId: _currentPost.id),
|
|
),
|
|
);
|
|
},
|
|
icon: Icon(
|
|
Icons.chat_bubble_outline,
|
|
color: Colors.grey[600],
|
|
size: 24,
|
|
),
|
|
),
|
|
Text(
|
|
'${_currentPost.comments}',
|
|
style: TextStyle(
|
|
color: Colors.grey[700],
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
Spacer(),
|
|
IconButton(
|
|
onPressed: () => _sharePost(_currentPost),
|
|
icon: Icon(
|
|
Icons.share_outlined,
|
|
color: Colors.grey[600],
|
|
size: 24,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ProfilePostsList extends StatefulWidget {
|
|
final Future<Map<String, dynamic>> Function() fetchPosts;
|
|
final Function(int posts, int likes)? onStatsUpdate;
|
|
|
|
const ProfilePostsList({
|
|
Key? key,
|
|
required this.fetchPosts,
|
|
this.onStatsUpdate,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
_ProfilePostsListState createState() => _ProfilePostsListState();
|
|
}
|
|
|
|
class _ProfilePostsListState extends State<ProfilePostsList> {
|
|
bool _isLoading = true;
|
|
List<Post> _posts = [];
|
|
String _errorMessage = '';
|
|
StreamSubscription<List<Post>>? _userPostsStreamSubscription;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_setupUserPostsStream();
|
|
_loadPosts();
|
|
// Start user posts polling when profile page is active
|
|
PostService.startUserPostsPolling();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_userPostsStreamSubscription?.cancel();
|
|
// Stop user posts polling when profile page is disposed
|
|
PostService.stopUserPostsPolling();
|
|
super.dispose();
|
|
}
|
|
|
|
void _setupUserPostsStream() {
|
|
print('Setting up user posts stream for profile');
|
|
_userPostsStreamSubscription?.cancel(); // Cancel any existing subscription
|
|
|
|
_userPostsStreamSubscription = PostService.getUserPostsStream().listen(
|
|
(updatedPosts) {
|
|
print(
|
|
'📱 User posts stream received data: ${updatedPosts.length} posts',
|
|
);
|
|
if (mounted) {
|
|
// Update the posts directly instead of fetching again
|
|
setState(() {
|
|
_posts = updatedPosts;
|
|
_isLoading = false;
|
|
_errorMessage = '';
|
|
});
|
|
// Update parent stats
|
|
if (widget.onStatsUpdate != null) {
|
|
final totalLikes = _posts.fold(0, (sum, post) => sum + post.likes);
|
|
widget.onStatsUpdate!(_posts.length, totalLikes);
|
|
}
|
|
print(
|
|
'🎯 User posts UI updated with ${updatedPosts.length} posts - setState called',
|
|
);
|
|
} else {
|
|
print('⚠️ Widget not mounted, skipping UI update');
|
|
}
|
|
},
|
|
onError: (error) {
|
|
print('User posts stream error: $error');
|
|
if (mounted) {
|
|
setState(() {
|
|
_errorMessage = 'Stream error: $error';
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
// Trigger stream with any existing cached data
|
|
Future.delayed(Duration(milliseconds: 100), () {
|
|
if (mounted) {
|
|
PostService.notifyUserPostsStreamWithCurrentData();
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> _loadPosts() async {
|
|
// Don't show loading for cached data unless it's the initial load
|
|
final isInitialLoad = _posts.isEmpty;
|
|
|
|
if (isInitialLoad) {
|
|
setState(() {
|
|
_isLoading = true;
|
|
_errorMessage = '';
|
|
});
|
|
}
|
|
|
|
try {
|
|
final result = await widget.fetchPosts();
|
|
if (mounted) {
|
|
setState(() {
|
|
if (result['success']) {
|
|
_posts = result['posts'];
|
|
// Update parent stats
|
|
if (widget.onStatsUpdate != null) {
|
|
final totalLikes = _posts.fold(
|
|
0,
|
|
(sum, post) => sum + post.likes,
|
|
);
|
|
widget.onStatsUpdate!(_posts.length, totalLikes);
|
|
}
|
|
_errorMessage = '';
|
|
} else {
|
|
_errorMessage = result['message'] ?? 'Failed to load posts';
|
|
}
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_errorMessage = 'Network error: $e';
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_isLoading) {
|
|
return Container(
|
|
padding: EdgeInsets.all(32),
|
|
child: Center(
|
|
child: CircularProgressIndicator(
|
|
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF6A4C93)),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (_errorMessage.isNotEmpty) {
|
|
return Container(
|
|
padding: EdgeInsets.all(32),
|
|
child: Center(
|
|
child: Column(
|
|
children: [
|
|
Icon(Icons.error_outline, size: 64, color: Colors.grey[400]),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
_errorMessage,
|
|
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
SizedBox(height: 16),
|
|
ElevatedButton(
|
|
onPressed: _loadPosts,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Color(0xFF6A4C93),
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: Text('Try Again'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (_posts.isEmpty) {
|
|
return Container(
|
|
padding: EdgeInsets.all(32),
|
|
child: Center(
|
|
child: Column(
|
|
children: [
|
|
Icon(Icons.post_add, size: 64, color: Colors.grey[400]),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
'No posts yet',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
color: Colors.grey[600],
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
SizedBox(height: 8),
|
|
Text(
|
|
'Start sharing your thoughts!',
|
|
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
children: [
|
|
..._posts.map(
|
|
(post) => Container(
|
|
margin: EdgeInsets.only(bottom: 16, left: 16, right: 16),
|
|
child: PostCard(post: post),
|
|
),
|
|
),
|
|
SizedBox(height: 24),
|
|
],
|
|
);
|
|
}
|
|
}
|