feat: cache and poll the user profile page
This commit is contained in:
parent
ff3ca9ab1f
commit
e73bc88879
@ -45,10 +45,14 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
print('Setting up user profile stream');
|
||||
_userStreamSubscription = UserService.getUserStream().listen(
|
||||
(updatedUserData) {
|
||||
print('Cache change detected - triggering refresh like refresh button');
|
||||
print('📱 User profile stream received data update');
|
||||
if (mounted) {
|
||||
// Force refresh like the refresh button
|
||||
_loadUserData(forceRefresh: true);
|
||||
// Update UI directly with stream data instead of making API call
|
||||
setState(() {
|
||||
userData = updatedUserData;
|
||||
isLoadingUser = false;
|
||||
});
|
||||
// Only reload stats which are from posts service
|
||||
_loadUserStats();
|
||||
}
|
||||
},
|
||||
@ -56,6 +60,13 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
print('User profile stream error: $error');
|
||||
},
|
||||
);
|
||||
|
||||
// Trigger stream with any existing cached data
|
||||
Future.delayed(Duration(milliseconds: 100), () {
|
||||
if (mounted) {
|
||||
UserService.notifyStreamWithCurrentData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadFCMToken() async {
|
||||
@ -109,16 +120,18 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
Future<void> _loadUserStats() async {
|
||||
final result = await PostService.getUserPosts();
|
||||
|
||||
setState(() {
|
||||
if (result['success'] == true) {
|
||||
final posts = result['posts'] as List<Post>;
|
||||
totalPosts = posts.length;
|
||||
totalLikes = posts.fold(0, (sum, post) => sum + post.likes);
|
||||
} else {
|
||||
totalPosts = 0;
|
||||
totalLikes = 0;
|
||||
}
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
if (result['success'] == true) {
|
||||
final posts = result['posts'] as List<Post>;
|
||||
totalPosts = posts.length;
|
||||
totalLikes = posts.fold(0, (sum, post) => sum + post.likes);
|
||||
} else {
|
||||
totalPosts = 0;
|
||||
totalLikes = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _showErrorAlert(String message) {
|
||||
@ -294,7 +307,7 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
ProfilePostsList(
|
||||
fetchPosts: PostService.getUserPosts,
|
||||
fetchPosts: () => PostService.getUserPosts(),
|
||||
onStatsUpdate: (posts, likes) {
|
||||
setState(() {
|
||||
totalPosts = posts;
|
||||
|
||||
@ -28,12 +28,14 @@ class CacheService {
|
||||
// Cache durations
|
||||
static const Duration _postsCacheDuration = Duration(seconds: 30);
|
||||
static const Duration _invitationsCacheDuration = Duration(seconds: 30);
|
||||
static const Duration _userCacheDuration = Duration(hours: 3);
|
||||
static const Duration _userCacheDuration = Duration(hours: 6);
|
||||
static const Duration _userPostsCacheDuration = Duration(seconds: 30);
|
||||
|
||||
// Polling intervals
|
||||
static const Duration _postsPollingInterval = Duration(seconds: 5);
|
||||
static const Duration _invitationsPollingInterval = Duration(seconds: 5);
|
||||
static const Duration _userPollingInterval = Duration(hours: 3);
|
||||
static const Duration _userPostsPollingInterval = Duration(seconds: 5);
|
||||
|
||||
/// Get cached data if available and not expired, otherwise return null
|
||||
static T? getCached<T>(String key) {
|
||||
@ -212,6 +214,9 @@ class CacheService {
|
||||
if (key == postsAllKey && data is List<Post>) {
|
||||
print('📱 Notifying posts stream with ${data.length} posts');
|
||||
notifyStreamListeners<List<Post>>(key, data);
|
||||
} else if (key == userPostsKey && data is List<Post>) {
|
||||
print('📱 Notifying user posts stream with ${data.length} posts');
|
||||
notifyStreamListeners<List<Post>>(key, data);
|
||||
} else if (key == invitationsAllKey && data is InvitationsData) {
|
||||
print('📱 Notifying invitations stream');
|
||||
notifyStreamListeners<InvitationsData>(key, data);
|
||||
@ -291,13 +296,16 @@ class CacheService {
|
||||
static const String postsAllKey = 'posts_all';
|
||||
static const String invitationsAllKey = 'invitations_all';
|
||||
static const String userProfileKey = 'user_profile';
|
||||
static const String userPostsKey = 'user_posts';
|
||||
|
||||
// Helper methods for specific cache durations
|
||||
static Duration get postsCacheDuration => _postsCacheDuration;
|
||||
static Duration get invitationsCacheDuration => _invitationsCacheDuration;
|
||||
static Duration get userCacheDuration => _userCacheDuration;
|
||||
static Duration get userPostsCacheDuration => _userPostsCacheDuration;
|
||||
|
||||
static Duration get postsPollingInterval => _postsPollingInterval;
|
||||
static Duration get invitationsPollingInterval => _invitationsPollingInterval;
|
||||
static Duration get userPollingInterval => _userPollingInterval;
|
||||
static Duration get userPostsPollingInterval => _userPostsPollingInterval;
|
||||
}
|
||||
@ -7,6 +7,7 @@ import 'cache_service.dart';
|
||||
|
||||
class PostService {
|
||||
static bool _pollingStarted = false;
|
||||
static bool _userPostsPollingStarted = false;
|
||||
|
||||
/// Get all posts with caching - returns cached data if available and fresh
|
||||
static Future<Map<String, dynamic>> getAllPosts({
|
||||
@ -139,29 +140,126 @@ class PostService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Start background polling for user posts
|
||||
static void startUserPostsPolling() {
|
||||
_userPostsPollingStarted = true;
|
||||
CacheService.startPolling(
|
||||
CacheService.userPostsKey,
|
||||
() => _fetchAndCacheUserPosts(true), // Mark as from polling
|
||||
CacheService.userPostsPollingInterval,
|
||||
CacheService.userPostsCacheDuration,
|
||||
);
|
||||
}
|
||||
|
||||
/// Stop background polling for user posts
|
||||
static void stopUserPostsPolling() {
|
||||
CacheService.stopPolling(CacheService.userPostsKey);
|
||||
_userPostsPollingStarted = false;
|
||||
}
|
||||
|
||||
/// Get user posts stream for real-time updates
|
||||
static Stream<List<Post>> getUserPostsStream() {
|
||||
return CacheService.getStream<List<Post>>(CacheService.userPostsKey);
|
||||
}
|
||||
|
||||
/// Force notify user posts stream with current cached data
|
||||
static void notifyUserPostsStreamWithCurrentData() {
|
||||
final cached = CacheService.getCachedStale<List<Post>>(
|
||||
CacheService.userPostsKey,
|
||||
);
|
||||
if (cached != null) {
|
||||
// Re-notify the stream with current data
|
||||
CacheService.notifyStreamListeners<List<Post>>(
|
||||
CacheService.userPostsKey,
|
||||
cached,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if polling is active
|
||||
static bool get isPollingActive => _pollingStarted;
|
||||
|
||||
/// Check if user posts polling is active
|
||||
static bool get isUserPostsPollingActive => _userPostsPollingStarted;
|
||||
|
||||
/// Invalidate posts cache (useful after creating/updating posts)
|
||||
static void invalidateCache() {
|
||||
CacheService.clearCache(CacheService.postsAllKey);
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> getUserPosts() async {
|
||||
/// Invalidate user posts cache (useful after creating posts)
|
||||
static void invalidateUserPostsCache() {
|
||||
CacheService.clearCache(CacheService.userPostsKey);
|
||||
}
|
||||
|
||||
/// Get user posts with caching - returns cached data if available and fresh
|
||||
static Future<Map<String, dynamic>> getUserPosts({
|
||||
bool forceRefresh = false,
|
||||
}) async {
|
||||
// Return cached data if available and not forcing refresh
|
||||
if (!forceRefresh) {
|
||||
final cached = CacheService.getCached<List<Post>>(
|
||||
CacheService.userPostsKey,
|
||||
);
|
||||
if (cached != null &&
|
||||
!CacheService.isCacheExpired(
|
||||
CacheService.userPostsKey,
|
||||
CacheService.userPostsCacheDuration,
|
||||
)) {
|
||||
return {
|
||||
'success': true,
|
||||
'message': 'Loaded from cache',
|
||||
'posts': cached,
|
||||
};
|
||||
}
|
||||
|
||||
// Return stale data while fetching fresh data in background
|
||||
final stale = CacheService.getCachedStale<List<Post>>(
|
||||
CacheService.userPostsKey,
|
||||
);
|
||||
if (stale != null && !forceRefresh) {
|
||||
// Trigger background refresh
|
||||
_fetchAndCacheUserPosts();
|
||||
return {
|
||||
'success': true,
|
||||
'message': 'Loaded from cache (refreshing in background)',
|
||||
'posts': stale,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch fresh data
|
||||
return await _fetchAndCacheUserPosts();
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> _fetchAndCacheUserPosts([bool fromPolling = false]) async {
|
||||
try {
|
||||
final response = await HttpService.get('/posts/user');
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final posts = (responseData['data'] as List?)
|
||||
?.map((post) => Post.fromJson(post))
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
// Only cache when not called from polling to prevent conflicts
|
||||
if (!fromPolling) {
|
||||
CacheService.setCached(CacheService.userPostsKey, posts);
|
||||
}
|
||||
|
||||
return {
|
||||
'success': responseData['status'] ?? false,
|
||||
'message': responseData['message'] ?? '',
|
||||
'posts':
|
||||
(responseData['data'] as List?)
|
||||
?.map((post) => Post.fromJson(post))
|
||||
.toList() ??
|
||||
[],
|
||||
'posts': posts,
|
||||
};
|
||||
} else if (response.statusCode == 401 || response.statusCode == 403) {
|
||||
await AuthService.handleAuthenticationError();
|
||||
return {
|
||||
'success': false,
|
||||
'message': 'Session expired. Please login again.',
|
||||
'posts': <Post>[],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
@ -191,8 +289,9 @@ class PostService {
|
||||
final responseData = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
// Invalidate posts cache since we added a new post
|
||||
// Invalidate both posts caches since we added a new post
|
||||
invalidateCache();
|
||||
invalidateUserPostsCache();
|
||||
|
||||
return {
|
||||
'success': responseData['status'] ?? false,
|
||||
|
||||
@ -110,6 +110,20 @@ class UserService {
|
||||
return CacheService.getStream<Map<String, dynamic>>(CacheService.userProfileKey);
|
||||
}
|
||||
|
||||
/// Force notify stream with current cached data (useful for initial stream setup)
|
||||
static void notifyStreamWithCurrentData() {
|
||||
final cached = CacheService.getCachedStale<Map<String, dynamic>>(
|
||||
CacheService.userProfileKey,
|
||||
);
|
||||
if (cached != null) {
|
||||
// Re-notify the stream with current data
|
||||
CacheService.notifyStreamListeners<Map<String, dynamic>>(
|
||||
CacheService.userProfileKey,
|
||||
cached,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if polling is active
|
||||
static bool get isPollingActive => _pollingStarted;
|
||||
|
||||
|
||||
@ -511,40 +511,104 @@ 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();
|
||||
}
|
||||
|
||||
Future<void> _loadPosts() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = '';
|
||||
});
|
||||
@override
|
||||
void dispose() {
|
||||
_userPostsStreamSubscription?.cancel();
|
||||
// Stop user posts polling when profile page is disposed
|
||||
PostService.stopUserPostsPolling();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
try {
|
||||
final result = await widget.fetchPosts();
|
||||
setState(() {
|
||||
if (result['success']) {
|
||||
_posts = result['posts'];
|
||||
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 {
|
||||
_errorMessage = result['message'] ?? 'Failed to load posts';
|
||||
print('⚠️ Widget not mounted, skipping UI update');
|
||||
}
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
},
|
||||
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(() {
|
||||
_errorMessage = 'Network error: $e';
|
||||
_isLoading = false;
|
||||
_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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user