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