feat: polling and caching for posts and invitations
This commit is contained in:
parent
e460cca64e
commit
ff3ca9ab1f
@ -6,6 +6,7 @@ import 'screens/notification_permission_screen.dart';
|
|||||||
import 'services/notification_service.dart';
|
import 'services/notification_service.dart';
|
||||||
import 'services/auth_service.dart';
|
import 'services/auth_service.dart';
|
||||||
import 'services/user_service.dart';
|
import 'services/user_service.dart';
|
||||||
|
import 'services/app_lifecycle_service.dart';
|
||||||
|
|
||||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
@ -13,6 +14,9 @@ void main() async {
|
|||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||||
|
|
||||||
|
// Initialize app lifecycle service
|
||||||
|
AppLifecycleService.initialize();
|
||||||
|
|
||||||
runApp(MyApp());
|
runApp(MyApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,6 +53,8 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
final userResult = await UserService.getCurrentUser();
|
final userResult = await UserService.getCurrentUser();
|
||||||
if (userResult['success'] == true) {
|
if (userResult['success'] == true) {
|
||||||
|
// Start polling services now that user is logged in and going to main screen
|
||||||
|
AppLifecycleService.startAllPolling();
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(builder: (context) => HomeScreen()),
|
MaterialPageRoute(builder: (context) => HomeScreen()),
|
||||||
);
|
);
|
||||||
@ -335,6 +341,9 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
if (result['success'] == true) {
|
if (result['success'] == true) {
|
||||||
final userResult = await UserService.getCurrentUser(forceRefresh: true);
|
final userResult = await UserService.getCurrentUser(forceRefresh: true);
|
||||||
|
|
||||||
|
// Start polling services now that user is logged in
|
||||||
|
AppLifecycleService.startAllPolling();
|
||||||
|
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => NotificationPermissionScreen(),
|
builder: (context) => NotificationPermissionScreen(),
|
||||||
|
|||||||
@ -19,6 +19,15 @@ class InvitationTag {
|
|||||||
iconName: json['iconName'],
|
iconName: json['iconName'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'colorHex': colorHex,
|
||||||
|
'iconName': iconName,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InvitationCreator {
|
class InvitationCreator {
|
||||||
@ -39,6 +48,14 @@ class InvitationCreator {
|
|||||||
avatar: json['avatar'],
|
avatar: json['avatar'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'displayName': displayName,
|
||||||
|
'avatar': avatar,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InvitationAttendee {
|
class InvitationAttendee {
|
||||||
@ -62,6 +79,15 @@ class InvitationAttendee {
|
|||||||
joinedAt: DateTime.parse(json['joinedAt']),
|
joinedAt: DateTime.parse(json['joinedAt']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'displayName': displayName,
|
||||||
|
'avatar': avatar,
|
||||||
|
'joinedAt': joinedAt.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Invitation {
|
class Invitation {
|
||||||
@ -103,6 +129,21 @@ class Invitation {
|
|||||||
createdAt: DateTime.parse(json['createdAt']),
|
createdAt: DateTime.parse(json['createdAt']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'dateTime': dateTime?.toIso8601String(),
|
||||||
|
'location': location,
|
||||||
|
'maxParticipants': maxParticipants,
|
||||||
|
'currentAttendees': currentAttendees,
|
||||||
|
'tag': tag.toJson(),
|
||||||
|
'creator': creator.toJson(),
|
||||||
|
'createdAt': createdAt.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InvitationsResponse {
|
class InvitationsResponse {
|
||||||
@ -150,6 +191,14 @@ class InvitationsData {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'created': created.map((item) => item.toJson()).toList(),
|
||||||
|
'accepted': accepted.map((item) => item.toJson()).toList(),
|
||||||
|
'available': available.map((item) => item.toJson()).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
bool get isEmpty => created.isEmpty && accepted.isEmpty && available.isEmpty;
|
bool get isEmpty => created.isEmpty && accepted.isEmpty && available.isEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,6 +247,22 @@ class InvitationDetails {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'dateTime': dateTime?.toIso8601String(),
|
||||||
|
'location': location,
|
||||||
|
'maxParticipants': maxParticipants,
|
||||||
|
'currentAttendees': currentAttendees,
|
||||||
|
'tag': tag.toJson(),
|
||||||
|
'creator': creator.toJson(),
|
||||||
|
'createdAt': createdAt.toIso8601String(),
|
||||||
|
'attendees': attendees.map((item) => item.toJson()).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
String get status {
|
String get status {
|
||||||
if (currentAttendees >= maxParticipants) {
|
if (currentAttendees >= maxParticipants) {
|
||||||
return 'Full';
|
return 'Full';
|
||||||
|
|||||||
@ -35,7 +35,7 @@ class _FeedPageState extends State<FeedPage> {
|
|||||||
),
|
),
|
||||||
body: PostsList(
|
body: PostsList(
|
||||||
key: _postsListKey,
|
key: _postsListKey,
|
||||||
fetchPosts: PostService.getAllPosts,
|
fetchPosts: ({bool forceRefresh = false}) => PostService.getAllPosts(forceRefresh: forceRefresh),
|
||||||
emptyStateTitle: 'Nothing here..',
|
emptyStateTitle: 'Nothing here..',
|
||||||
emptyStateSubtitle: 'Create the first post!',
|
emptyStateSubtitle: 'Create the first post!',
|
||||||
showRefreshIndicator: true,
|
showRefreshIndicator: true,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:async';
|
||||||
import '../../services/invitations_service.dart';
|
import '../../services/invitations_service.dart';
|
||||||
import '../../models/invitation_models.dart';
|
import '../../models/invitation_models.dart';
|
||||||
import '../../utils/invitation_utils.dart';
|
import '../../utils/invitation_utils.dart';
|
||||||
@ -17,11 +18,13 @@ class _InvitationsPageState extends State<InvitationsPage>
|
|||||||
Map<String, bool> _acceptingInvitations = {};
|
Map<String, bool> _acceptingInvitations = {};
|
||||||
Map<String, bool> _acceptedInvitations = {};
|
Map<String, bool> _acceptedInvitations = {};
|
||||||
Map<String, AnimationController> _animationControllers = {};
|
Map<String, AnimationController> _animationControllers = {};
|
||||||
|
StreamSubscription<InvitationsData>? _invitationsStreamSubscription;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadInvitations();
|
_loadInvitations();
|
||||||
|
_setupInvitationsStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -29,25 +32,55 @@ class _InvitationsPageState extends State<InvitationsPage>
|
|||||||
for (final controller in _animationControllers.values) {
|
for (final controller in _animationControllers.values) {
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
}
|
}
|
||||||
|
_invitationsStreamSubscription?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadInvitations() async {
|
void _setupInvitationsStream() {
|
||||||
|
print('Setting up invitations stream');
|
||||||
|
_invitationsStreamSubscription = InvitationsService.getInvitationsStream()
|
||||||
|
.listen(
|
||||||
|
(updatedInvitationsData) {
|
||||||
|
print('Invitations stream received updated data');
|
||||||
|
if (mounted) {
|
||||||
|
// Update the invitations directly instead of fetching again
|
||||||
|
setState(() {
|
||||||
|
_invitationsData = updatedInvitationsData;
|
||||||
|
_isLoading = false;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
print('Invitations UI updated with ${updatedInvitationsData.created.length + updatedInvitationsData.accepted.length + updatedInvitationsData.available.length} total invitations');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) {
|
||||||
|
print('Invitations stream error: $error');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadInvitations({bool forceRefresh = false}) async {
|
||||||
|
// Don't show loading for cached data unless forcing refresh
|
||||||
|
final isInitialLoad = _invitationsData == null;
|
||||||
|
|
||||||
|
if (isInitialLoad || forceRefresh) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
final result = await InvitationsService.getAllInvitations();
|
final result = await InvitationsService.getAllInvitations(forceRefresh: forceRefresh);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
|
||||||
if (result['success']) {
|
if (result['success']) {
|
||||||
_invitationsData = result['data'];
|
final invitationsData = result['invitations'] as InvitationsData;
|
||||||
|
_invitationsData = invitationsData;
|
||||||
|
_errorMessage = null;
|
||||||
} else {
|
} else {
|
||||||
_errorMessage = result['message'];
|
_errorMessage = result['message'];
|
||||||
}
|
}
|
||||||
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -130,7 +163,7 @@ class _InvitationsPageState extends State<InvitationsPage>
|
|||||||
|
|
||||||
Future.delayed(Duration(milliseconds: 2000), () {
|
Future.delayed(Duration(milliseconds: 2000), () {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_loadInvitations();
|
_loadInvitations(forceRefresh: true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -161,7 +194,7 @@ class _InvitationsPageState extends State<InvitationsPage>
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
_loadInvitations();
|
_loadInvitations(forceRefresh: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -510,7 +543,7 @@ class _InvitationsPageState extends State<InvitationsPage>
|
|||||||
),
|
),
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(icon: Icon(Icons.refresh), onPressed: _loadInvitations),
|
IconButton(icon: Icon(Icons.refresh), onPressed: () => _loadInvitations(forceRefresh: true)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: _isLoading
|
body: _isLoading
|
||||||
@ -520,7 +553,7 @@ class _InvitationsPageState extends State<InvitationsPage>
|
|||||||
: _invitationsData?.isEmpty ?? true
|
: _invitationsData?.isEmpty ?? true
|
||||||
? _buildEmptyState()
|
? _buildEmptyState()
|
||||||
: RefreshIndicator(
|
: RefreshIndicator(
|
||||||
onRefresh: _loadInvitations,
|
onRefresh: () => _loadInvitations(forceRefresh: true),
|
||||||
color: Color(0xFF6A4C93),
|
color: Color(0xFF6A4C93),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
physics: AlwaysScrollableScrollPhysics(),
|
||||||
@ -554,7 +587,7 @@ class _InvitationsPageState extends State<InvitationsPage>
|
|||||||
MaterialPageRoute(builder: (context) => CreateInvitationPage()),
|
MaterialPageRoute(builder: (context) => CreateInvitationPage()),
|
||||||
);
|
);
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
_loadInvitations();
|
_loadInvitations(forceRefresh: true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
backgroundColor: Color(0xFF6A4C93),
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'dart:async';
|
||||||
import '../../services/notification_service.dart';
|
import '../../services/notification_service.dart';
|
||||||
import '../../services/user_service.dart';
|
import '../../services/user_service.dart';
|
||||||
import '../../services/auth_service.dart';
|
import '../../services/auth_service.dart';
|
||||||
import '../../services/post_service.dart';
|
import '../../services/post_service.dart';
|
||||||
|
import '../../services/app_lifecycle_service.dart';
|
||||||
import '../../models/post_models.dart';
|
import '../../models/post_models.dart';
|
||||||
import '../../widgets/posts_list.dart';
|
import '../../widgets/posts_list.dart';
|
||||||
import '../../utils/password_generator.dart';
|
import '../../utils/password_generator.dart';
|
||||||
@ -21,6 +23,7 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
bool isLoadingUser = true;
|
bool isLoadingUser = true;
|
||||||
int totalLikes = 0;
|
int totalLikes = 0;
|
||||||
int totalPosts = 0;
|
int totalPosts = 0;
|
||||||
|
StreamSubscription<Map<String, dynamic>>? _userStreamSubscription;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -28,14 +31,33 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
_loadFCMToken();
|
_loadFCMToken();
|
||||||
_loadUserData();
|
_loadUserData();
|
||||||
_loadUserStats();
|
_loadUserStats();
|
||||||
|
_setupUserStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_tokenController.dispose();
|
_tokenController.dispose();
|
||||||
|
_userStreamSubscription?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setupUserStream() {
|
||||||
|
print('Setting up user profile stream');
|
||||||
|
_userStreamSubscription = UserService.getUserStream().listen(
|
||||||
|
(updatedUserData) {
|
||||||
|
print('Cache change detected - triggering refresh like refresh button');
|
||||||
|
if (mounted) {
|
||||||
|
// Force refresh like the refresh button
|
||||||
|
_loadUserData(forceRefresh: true);
|
||||||
|
_loadUserStats();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) {
|
||||||
|
print('User profile stream error: $error');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadFCMToken() async {
|
Future<void> _loadFCMToken() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
@ -57,23 +79,32 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadUserData() async {
|
Future<void> _loadUserData({bool forceRefresh = false}) async {
|
||||||
|
// Don't show loading for cached data unless forcing refresh
|
||||||
|
final isInitialLoad = userData == null;
|
||||||
|
|
||||||
|
if (isInitialLoad || forceRefresh) {
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoadingUser = true;
|
isLoadingUser = true;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
final result = await UserService.getCurrentUser();
|
final result = await UserService.getCurrentUser(forceRefresh: forceRefresh);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoadingUser = false;
|
|
||||||
if (result['success'] == true) {
|
if (result['success'] == true) {
|
||||||
userData = result['data'];
|
userData = result['data'];
|
||||||
} else {
|
} else {
|
||||||
userData = null;
|
userData = null;
|
||||||
|
if (isInitialLoad) {
|
||||||
_showErrorAlert(result['message'] ?? 'Failed to load user data');
|
_showErrorAlert(result['message'] ?? 'Failed to load user data');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
isLoadingUser = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadUserStats() async {
|
Future<void> _loadUserStats() async {
|
||||||
final result = await PostService.getUserPosts();
|
final result = await PostService.getUserPosts();
|
||||||
@ -425,6 +456,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
// Stop polling services before logout
|
||||||
|
AppLifecycleService.dispose();
|
||||||
await AuthService.logout();
|
await AuthService.logout();
|
||||||
|
|
||||||
Navigator.of(
|
Navigator.of(
|
||||||
|
|||||||
101
frontend/lib/services/app_lifecycle_service.dart
Normal file
101
frontend/lib/services/app_lifecycle_service.dart
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'cache_service.dart';
|
||||||
|
import 'post_service.dart';
|
||||||
|
import 'invitations_service.dart';
|
||||||
|
import 'user_service.dart';
|
||||||
|
|
||||||
|
class AppLifecycleService extends WidgetsBindingObserver {
|
||||||
|
static final AppLifecycleService _instance = AppLifecycleService._internal();
|
||||||
|
factory AppLifecycleService() => _instance;
|
||||||
|
AppLifecycleService._internal();
|
||||||
|
|
||||||
|
bool _isInitialized = false;
|
||||||
|
|
||||||
|
/// Initialize the app lifecycle service
|
||||||
|
static void initialize() {
|
||||||
|
if (!_instance._isInitialized) {
|
||||||
|
WidgetsBinding.instance.addObserver(_instance);
|
||||||
|
_instance._isInitialized = true;
|
||||||
|
|
||||||
|
// DO NOT start polling automatically - only when user is logged in and on main screen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start all polling services (call this only when user is logged in and on main tabs)
|
||||||
|
static void startAllPolling() {
|
||||||
|
PostService.startPolling();
|
||||||
|
InvitationsService.startPolling();
|
||||||
|
UserService.startPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispose the app lifecycle service
|
||||||
|
static void dispose() {
|
||||||
|
if (_instance._isInitialized) {
|
||||||
|
WidgetsBinding.instance.removeObserver(_instance);
|
||||||
|
_instance._isInitialized = false;
|
||||||
|
|
||||||
|
// Stop all polling
|
||||||
|
CacheService.stopAllPolling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
super.didChangeAppLifecycleState(state);
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case AppLifecycleState.resumed:
|
||||||
|
_onAppResumed();
|
||||||
|
break;
|
||||||
|
case AppLifecycleState.paused:
|
||||||
|
_onAppPaused();
|
||||||
|
break;
|
||||||
|
case AppLifecycleState.inactive:
|
||||||
|
// App is transitioning, do nothing
|
||||||
|
break;
|
||||||
|
case AppLifecycleState.detached:
|
||||||
|
_onAppDetached();
|
||||||
|
break;
|
||||||
|
case AppLifecycleState.hidden:
|
||||||
|
// App is hidden, treat similar to paused
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAppResumed() {
|
||||||
|
// Only restart polling if it was already running (user is logged in)
|
||||||
|
if (PostService.isPollingActive || InvitationsService.isPollingActive || UserService.isPollingActive) {
|
||||||
|
print('App resumed - restarting polling');
|
||||||
|
PostService.startPolling();
|
||||||
|
InvitationsService.startPolling();
|
||||||
|
UserService.startPolling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAppPaused() {
|
||||||
|
print('App paused - stopping polling');
|
||||||
|
// Stop polling to save battery when app goes to background
|
||||||
|
CacheService.stopAllPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAppDetached() {
|
||||||
|
print('App detached - cleaning up');
|
||||||
|
// Clean up when app is completely terminated
|
||||||
|
CacheService.stopAllPolling();
|
||||||
|
CacheService.clearAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Force refresh all cached data (useful for pull-to-refresh)
|
||||||
|
static Future<void> refreshAllData() async {
|
||||||
|
await Future.wait([
|
||||||
|
PostService.getAllPosts(forceRefresh: true),
|
||||||
|
InvitationsService.getAllInvitations(forceRefresh: true),
|
||||||
|
UserService.getCurrentUser(forceRefresh: true),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get cache status for debugging
|
||||||
|
static Map<String, dynamic> getCacheStatus() {
|
||||||
|
return CacheService.getCacheInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import '../constants/api_constants.dart';
|
import '../constants/api_constants.dart';
|
||||||
import '../main.dart';
|
import '../main.dart';
|
||||||
|
import 'app_lifecycle_service.dart';
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
static const FlutterSecureStorage _storage = FlutterSecureStorage();
|
static const FlutterSecureStorage _storage = FlutterSecureStorage();
|
||||||
@ -62,6 +63,8 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> logout() async {
|
static Future<void> logout() async {
|
||||||
|
// Stop all polling services when logging out
|
||||||
|
AppLifecycleService.dispose();
|
||||||
await _storage.delete(key: _tokenKey);
|
await _storage.delete(key: _tokenKey);
|
||||||
await _storage.delete(key: _userDataKey);
|
await _storage.delete(key: _userDataKey);
|
||||||
}
|
}
|
||||||
|
|||||||
303
frontend/lib/services/cache_service.dart
Normal file
303
frontend/lib/services/cache_service.dart
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import '../models/post_models.dart';
|
||||||
|
import '../models/invitation_models.dart';
|
||||||
|
|
||||||
|
class CachedData<T> {
|
||||||
|
final T data;
|
||||||
|
final DateTime timestamp;
|
||||||
|
final String jsonHash;
|
||||||
|
|
||||||
|
CachedData({
|
||||||
|
required this.data,
|
||||||
|
required this.timestamp,
|
||||||
|
required this.jsonHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool isExpired(Duration maxAge) {
|
||||||
|
return DateTime.now().difference(timestamp) > maxAge;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CacheService {
|
||||||
|
static final Map<String, CachedData> _cache = {};
|
||||||
|
static final Map<String, Timer> _pollingTimers = {};
|
||||||
|
static final Map<String, StreamController> _streamControllers = {};
|
||||||
|
|
||||||
|
// Cache durations
|
||||||
|
static const Duration _postsCacheDuration = Duration(seconds: 30);
|
||||||
|
static const Duration _invitationsCacheDuration = Duration(seconds: 30);
|
||||||
|
static const Duration _userCacheDuration = Duration(hours: 3);
|
||||||
|
|
||||||
|
// Polling intervals
|
||||||
|
static const Duration _postsPollingInterval = Duration(seconds: 5);
|
||||||
|
static const Duration _invitationsPollingInterval = Duration(seconds: 5);
|
||||||
|
static const Duration _userPollingInterval = Duration(hours: 3);
|
||||||
|
|
||||||
|
/// Get cached data if available and not expired, otherwise return null
|
||||||
|
static T? getCached<T>(String key) {
|
||||||
|
final cached = _cache[key];
|
||||||
|
if (cached != null) {
|
||||||
|
return cached.data as T?;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get cached data regardless of expiration (for showing stale data while loading)
|
||||||
|
static T? getCachedStale<T>(String key) {
|
||||||
|
final cached = _cache[key];
|
||||||
|
if (cached != null) {
|
||||||
|
return cached.data as T?;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if cache is expired
|
||||||
|
static bool isCacheExpired(String key, Duration maxAge) {
|
||||||
|
final cached = _cache[key];
|
||||||
|
if (cached == null) return true;
|
||||||
|
return cached.isExpired(maxAge);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set cached data with JSON hash for change detection
|
||||||
|
static void setCached<T>(String key, T data) {
|
||||||
|
final jsonHash = _generateDataHash(data);
|
||||||
|
|
||||||
|
_cache[key] = CachedData(
|
||||||
|
data: data,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
jsonHash: jsonHash,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if data has changed by comparing hashes
|
||||||
|
static bool hasDataChanged(String key, dynamic newData) {
|
||||||
|
final cached = _cache[key];
|
||||||
|
if (cached == null) return true;
|
||||||
|
|
||||||
|
final newJsonHash = _generateDataHash(newData);
|
||||||
|
final hasChanged = cached.jsonHash != newJsonHash;
|
||||||
|
|
||||||
|
if (hasChanged) {
|
||||||
|
print(
|
||||||
|
'Data changed for $key: old hash ${cached.jsonHash}, new hash $newJsonHash',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
print('No data changes for $key: hash remains ${cached.jsonHash}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a reliable hash for data comparison
|
||||||
|
static String _generateDataHash(dynamic data) {
|
||||||
|
try {
|
||||||
|
String jsonString;
|
||||||
|
|
||||||
|
if (data is List) {
|
||||||
|
// Handle lists of objects (like List<Post>)
|
||||||
|
final jsonList = data.map((item) {
|
||||||
|
if (item is Map) {
|
||||||
|
return item;
|
||||||
|
} else if (item.runtimeType.toString().contains('Post')) {
|
||||||
|
// Call toJson() on Post objects
|
||||||
|
return (item as dynamic).toJson();
|
||||||
|
} else if (item.runtimeType.toString().contains('Invitation')) {
|
||||||
|
// Call toJson() on Invitation objects
|
||||||
|
return (item as dynamic).toJson();
|
||||||
|
} else {
|
||||||
|
return item.toString();
|
||||||
|
}
|
||||||
|
}).toList();
|
||||||
|
jsonString = jsonEncode(jsonList);
|
||||||
|
} else if (data is Map) {
|
||||||
|
jsonString = jsonEncode(data);
|
||||||
|
} else if (data.runtimeType.toString().contains('Post') ||
|
||||||
|
data.runtimeType.toString().contains('Invitation')) {
|
||||||
|
// Call toJson() on model objects
|
||||||
|
jsonString = jsonEncode((data as dynamic).toJson());
|
||||||
|
} else {
|
||||||
|
jsonString = data.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate MD5 hash for comparison
|
||||||
|
final bytes = utf8.encode(jsonString);
|
||||||
|
final digest = md5.convert(bytes);
|
||||||
|
final hashResult = digest.toString();
|
||||||
|
|
||||||
|
// Debug: Show first 100 chars of JSON for debugging
|
||||||
|
final debugJson = jsonString.length > 100
|
||||||
|
? jsonString.substring(0, 100) + '...'
|
||||||
|
: jsonString;
|
||||||
|
print('Generated hash $hashResult for JSON: $debugJson');
|
||||||
|
|
||||||
|
return hashResult;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error generating data hash: $e');
|
||||||
|
// Fallback to simple string hash
|
||||||
|
return data.toString().hashCode.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get stream for cache updates
|
||||||
|
static Stream<T> getStream<T>(String key) {
|
||||||
|
if (!_streamControllers.containsKey(key)) {
|
||||||
|
_streamControllers[key] = StreamController<T>.broadcast();
|
||||||
|
}
|
||||||
|
return (_streamControllers[key] as StreamController<T>).stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Notify stream listeners of cache updates (safely) - Force UI refresh
|
||||||
|
static void notifyStreamListeners<T>(String key, T data) {
|
||||||
|
try {
|
||||||
|
// Ensure we have a stream controller
|
||||||
|
if (!_streamControllers.containsKey(key)) {
|
||||||
|
print('Creating new stream controller for $key');
|
||||||
|
_streamControllers[key] = StreamController<T>.broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
final controller = _streamControllers[key] as StreamController<T>;
|
||||||
|
|
||||||
|
if (!controller.isClosed) {
|
||||||
|
print('Notifying stream listeners for $key with ${data.runtimeType}');
|
||||||
|
controller.add(data);
|
||||||
|
} else {
|
||||||
|
print('Stream controller for $key is closed, creating new one');
|
||||||
|
_streamControllers[key] = StreamController<T>.broadcast();
|
||||||
|
(_streamControllers[key] as StreamController<T>).add(data);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error notifying stream listeners for $key: $e');
|
||||||
|
// Recreate controller on error
|
||||||
|
try {
|
||||||
|
_streamControllers[key]?.close();
|
||||||
|
_streamControllers[key] = StreamController<T>.broadcast();
|
||||||
|
(_streamControllers[key] as StreamController<T>).add(data);
|
||||||
|
} catch (recreateError) {
|
||||||
|
print('Failed to recreate stream controller for $key: $recreateError');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start polling for a specific endpoint (silent - no UI notifications)
|
||||||
|
static void startPolling(
|
||||||
|
String key,
|
||||||
|
Future<Map<String, dynamic>> Function() fetchFunction,
|
||||||
|
Duration interval,
|
||||||
|
Duration cacheMaxAge,
|
||||||
|
) {
|
||||||
|
// Cancel existing timer if any
|
||||||
|
stopPolling(key);
|
||||||
|
|
||||||
|
_pollingTimers[key] = Timer.periodic(interval, (timer) async {
|
||||||
|
try {
|
||||||
|
print('Polling $key...');
|
||||||
|
final result = await fetchFunction();
|
||||||
|
|
||||||
|
if (result['success'] == true) {
|
||||||
|
// Use the parsed data from the result (e.g., result['posts'] for posts)
|
||||||
|
final data = result['posts'] ?? result['invitations'] ?? result['data'];
|
||||||
|
print('📊 Polling $key got data type: ${data.runtimeType}, length: ${data is List ? data.length : 'N/A'}');
|
||||||
|
|
||||||
|
// Check if data has changed BEFORE updating cache
|
||||||
|
final dataChanged = hasDataChanged(key, data);
|
||||||
|
|
||||||
|
if (dataChanged) {
|
||||||
|
print(
|
||||||
|
'🔄 Data changed for $key, updating cache and forcing UI refresh',
|
||||||
|
);
|
||||||
|
setCached(key, data);
|
||||||
|
// Force refresh by notifying stream listeners with appropriate types
|
||||||
|
if (key == postsAllKey && data is List<Post>) {
|
||||||
|
print('📱 Notifying 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);
|
||||||
|
} else {
|
||||||
|
print('📱 Notifying generic stream for $key with ${data.runtimeType}');
|
||||||
|
notifyStreamListeners<dynamic>(key, data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print('No data changes for $key - not notifying streams');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print('Polling failed for $key: ${result['message']}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Polling error for $key: $e');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop polling for a specific endpoint
|
||||||
|
static void stopPolling(String key) {
|
||||||
|
_pollingTimers[key]?.cancel();
|
||||||
|
_pollingTimers.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop all polling
|
||||||
|
static void stopAllPolling() {
|
||||||
|
for (final timer in _pollingTimers.values) {
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
|
_pollingTimers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all caches
|
||||||
|
static void clearAll() {
|
||||||
|
_cache.clear();
|
||||||
|
stopAllPolling();
|
||||||
|
_cleanupStreamControllers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safely cleanup stream controllers
|
||||||
|
static void _cleanupStreamControllers() {
|
||||||
|
for (final controller in _streamControllers.values) {
|
||||||
|
if (!controller.isClosed) {
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_streamControllers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clean up specific stream controller
|
||||||
|
static void cleanupStream(String key) {
|
||||||
|
if (_streamControllers.containsKey(key)) {
|
||||||
|
if (!_streamControllers[key]!.isClosed) {
|
||||||
|
_streamControllers[key]!.close();
|
||||||
|
}
|
||||||
|
_streamControllers.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear specific cache
|
||||||
|
static void clearCache(String key) {
|
||||||
|
_cache.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get cache info for debugging
|
||||||
|
static Map<String, dynamic> getCacheInfo() {
|
||||||
|
return {
|
||||||
|
'cacheSize': _cache.length,
|
||||||
|
'activePollers': _pollingTimers.length,
|
||||||
|
'activeStreams': _streamControllers.length,
|
||||||
|
'cacheKeys': _cache.keys.toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Predefined cache keys
|
||||||
|
static const String postsAllKey = 'posts_all';
|
||||||
|
static const String invitationsAllKey = 'invitations_all';
|
||||||
|
static const String userProfileKey = 'user_profile';
|
||||||
|
|
||||||
|
// Helper methods for specific cache durations
|
||||||
|
static Duration get postsCacheDuration => _postsCacheDuration;
|
||||||
|
static Duration get invitationsCacheDuration => _invitationsCacheDuration;
|
||||||
|
static Duration get userCacheDuration => _userCacheDuration;
|
||||||
|
|
||||||
|
static Duration get postsPollingInterval => _postsPollingInterval;
|
||||||
|
static Duration get invitationsPollingInterval => _invitationsPollingInterval;
|
||||||
|
static Duration get userPollingInterval => _userPollingInterval;
|
||||||
|
}
|
||||||
@ -3,9 +3,41 @@ import '../constants/api_constants.dart';
|
|||||||
import '../models/invitation_models.dart';
|
import '../models/invitation_models.dart';
|
||||||
import 'http_service.dart';
|
import 'http_service.dart';
|
||||||
import 'auth_service.dart';
|
import 'auth_service.dart';
|
||||||
|
import 'cache_service.dart';
|
||||||
|
|
||||||
class InvitationsService {
|
class InvitationsService {
|
||||||
static Future<Map<String, dynamic>> getAllInvitations() async {
|
static bool _pollingStarted = false;
|
||||||
|
/// Get all invitations with caching - returns cached data if available and fresh
|
||||||
|
static Future<Map<String, dynamic>> getAllInvitations({bool forceRefresh = false}) async {
|
||||||
|
// Return cached data if available and not forcing refresh
|
||||||
|
if (!forceRefresh) {
|
||||||
|
final cached = CacheService.getCached<InvitationsData>(CacheService.invitationsAllKey);
|
||||||
|
if (cached != null && !CacheService.isCacheExpired(CacheService.invitationsAllKey, CacheService.invitationsCacheDuration)) {
|
||||||
|
return {
|
||||||
|
'success': true,
|
||||||
|
'message': 'Loaded from cache',
|
||||||
|
'invitations': cached,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return stale data while fetching fresh data in background
|
||||||
|
final stale = CacheService.getCachedStale<InvitationsData>(CacheService.invitationsAllKey);
|
||||||
|
if (stale != null && !forceRefresh) {
|
||||||
|
// Trigger background refresh
|
||||||
|
_fetchAndCacheInvitations();
|
||||||
|
return {
|
||||||
|
'success': true,
|
||||||
|
'message': 'Loaded from cache (refreshing in background)',
|
||||||
|
'invitations': stale,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch fresh data
|
||||||
|
return await _fetchAndCacheInvitations();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> _fetchAndCacheInvitations([bool fromPolling = false]) async {
|
||||||
try {
|
try {
|
||||||
final response = await HttpService.get(
|
final response = await HttpService.get(
|
||||||
ApiConstants.getAllInvitationsEndpoint,
|
ApiConstants.getAllInvitationsEndpoint,
|
||||||
@ -16,12 +48,23 @@ class InvitationsService {
|
|||||||
final invitationsResponse = InvitationsResponse.fromJson(responseData);
|
final invitationsResponse = InvitationsResponse.fromJson(responseData);
|
||||||
|
|
||||||
if (invitationsResponse.status) {
|
if (invitationsResponse.status) {
|
||||||
return {'success': true, 'data': invitationsResponse.data};
|
final invitationsData = invitationsResponse.data;
|
||||||
|
|
||||||
|
// Only cache when not called from polling to prevent conflicts
|
||||||
|
if (!fromPolling) {
|
||||||
|
CacheService.setCached(CacheService.invitationsAllKey, invitationsData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': true,
|
||||||
|
'invitations': invitationsData,
|
||||||
|
'message': invitationsResponse.message ?? '',
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
'success': false,
|
'success': false,
|
||||||
'message':
|
'message': invitationsResponse.message ?? 'Failed to fetch invitations',
|
||||||
invitationsResponse.message ?? 'Failed to fetch invitations',
|
'invitations': InvitationsData(created: [], accepted: [], available: []),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if (response.statusCode == 401 || response.statusCode == 403) {
|
} else if (response.statusCode == 401 || response.statusCode == 403) {
|
||||||
@ -29,11 +72,13 @@ class InvitationsService {
|
|||||||
return {
|
return {
|
||||||
'success': false,
|
'success': false,
|
||||||
'message': 'Session expired. Please login again.',
|
'message': 'Session expired. Please login again.',
|
||||||
|
'invitations': InvitationsData(created: [], accepted: [], available: []),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
'success': false,
|
'success': false,
|
||||||
'message': 'Server error (${response.statusCode})',
|
'message': 'Server error (${response.statusCode})',
|
||||||
|
'invitations': InvitationsData(created: [], accepted: [], available: []),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -41,10 +86,41 @@ class InvitationsService {
|
|||||||
return {
|
return {
|
||||||
'success': false,
|
'success': false,
|
||||||
'message': 'Network error. Please check your connection.',
|
'message': 'Network error. Please check your connection.',
|
||||||
|
'invitations': InvitationsData(created: [], accepted: [], available: []),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Start background polling for invitations
|
||||||
|
static void startPolling() {
|
||||||
|
_pollingStarted = true;
|
||||||
|
CacheService.startPolling(
|
||||||
|
CacheService.invitationsAllKey,
|
||||||
|
() => _fetchAndCacheInvitations(true), // Mark as from polling
|
||||||
|
CacheService.invitationsPollingInterval,
|
||||||
|
CacheService.invitationsCacheDuration,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop background polling for invitations
|
||||||
|
static void stopPolling() {
|
||||||
|
CacheService.stopPolling(CacheService.invitationsAllKey);
|
||||||
|
_pollingStarted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get invitations stream for real-time updates
|
||||||
|
static Stream<InvitationsData> getInvitationsStream() {
|
||||||
|
return CacheService.getStream<InvitationsData>(CacheService.invitationsAllKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if polling is active
|
||||||
|
static bool get isPollingActive => _pollingStarted;
|
||||||
|
|
||||||
|
/// Invalidate invitations cache (useful after creating/updating invitations)
|
||||||
|
static void invalidateCache() {
|
||||||
|
CacheService.clearCache(CacheService.invitationsAllKey);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<Map<String, dynamic>> acceptInvitation(int invitationId) async {
|
static Future<Map<String, dynamic>> acceptInvitation(int invitationId) async {
|
||||||
try {
|
try {
|
||||||
final response = await HttpService.post(
|
final response = await HttpService.post(
|
||||||
@ -54,6 +130,8 @@ class InvitationsService {
|
|||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final responseData = jsonDecode(response.body);
|
final responseData = jsonDecode(response.body);
|
||||||
|
// Invalidate invitations cache since invitation status changed
|
||||||
|
invalidateCache();
|
||||||
return {
|
return {
|
||||||
'success': responseData['status'] ?? false,
|
'success': responseData['status'] ?? false,
|
||||||
'message': responseData['message'] ?? 'Request completed',
|
'message': responseData['message'] ?? 'Request completed',
|
||||||
@ -93,6 +171,8 @@ class InvitationsService {
|
|||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final responseData = jsonDecode(response.body);
|
final responseData = jsonDecode(response.body);
|
||||||
if (responseData['status'] == true) {
|
if (responseData['status'] == true) {
|
||||||
|
// Invalidate invitations cache since new invitation was created
|
||||||
|
invalidateCache();
|
||||||
return {
|
return {
|
||||||
'success': true,
|
'success': true,
|
||||||
'data': responseData['data'],
|
'data': responseData['data'],
|
||||||
|
|||||||
@ -3,22 +3,72 @@ import '../models/post_models.dart';
|
|||||||
import '../models/comment_models.dart';
|
import '../models/comment_models.dart';
|
||||||
import 'http_service.dart';
|
import 'http_service.dart';
|
||||||
import 'auth_service.dart';
|
import 'auth_service.dart';
|
||||||
|
import 'cache_service.dart';
|
||||||
|
|
||||||
class PostService {
|
class PostService {
|
||||||
static Future<Map<String, dynamic>> getAllPosts() async {
|
static bool _pollingStarted = false;
|
||||||
|
|
||||||
|
/// Get all posts with caching - returns cached data if available and fresh
|
||||||
|
static Future<Map<String, dynamic>> getAllPosts({
|
||||||
|
bool forceRefresh = false,
|
||||||
|
}) async {
|
||||||
|
// Return cached data if available and not forcing refresh
|
||||||
|
if (!forceRefresh) {
|
||||||
|
final cached = CacheService.getCached<List<Post>>(
|
||||||
|
CacheService.postsAllKey,
|
||||||
|
);
|
||||||
|
if (cached != null &&
|
||||||
|
!CacheService.isCacheExpired(
|
||||||
|
CacheService.postsAllKey,
|
||||||
|
CacheService.postsCacheDuration,
|
||||||
|
)) {
|
||||||
|
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.postsAllKey,
|
||||||
|
);
|
||||||
|
if (stale != null && !forceRefresh) {
|
||||||
|
// Trigger background refresh
|
||||||
|
_fetchAndCachePosts();
|
||||||
|
return {
|
||||||
|
'success': true,
|
||||||
|
'message': 'Loaded from cache (refreshing in background)',
|
||||||
|
'posts': stale,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch fresh data
|
||||||
|
return await _fetchAndCachePosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> _fetchAndCachePosts([bool fromPolling = false]) async {
|
||||||
try {
|
try {
|
||||||
final response = await HttpService.get('/posts/all');
|
final response = await HttpService.get('/posts/all');
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final responseData = jsonDecode(response.body);
|
final responseData = jsonDecode(response.body);
|
||||||
return {
|
final posts =
|
||||||
'success': responseData['status'] ?? false,
|
|
||||||
'message': responseData['message'] ?? '',
|
|
||||||
'posts':
|
|
||||||
(responseData['data'] as List?)
|
(responseData['data'] as List?)
|
||||||
?.map((post) => Post.fromJson(post))
|
?.map((post) => Post.fromJson(post))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[],
|
[];
|
||||||
|
|
||||||
|
// Only cache when not called from polling to prevent conflicts
|
||||||
|
if (!fromPolling) {
|
||||||
|
CacheService.setCached(CacheService.postsAllKey, posts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': responseData['status'] ?? false,
|
||||||
|
'message': responseData['message'] ?? '',
|
||||||
|
'posts': posts,
|
||||||
};
|
};
|
||||||
} else if (response.statusCode == 401 || response.statusCode == 403) {
|
} else if (response.statusCode == 401 || response.statusCode == 403) {
|
||||||
await AuthService.handleAuthenticationError();
|
await AuthService.handleAuthenticationError();
|
||||||
@ -53,6 +103,50 @@ class PostService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Start background polling for posts
|
||||||
|
static void startPolling() {
|
||||||
|
_pollingStarted = true;
|
||||||
|
CacheService.startPolling(
|
||||||
|
CacheService.postsAllKey,
|
||||||
|
() => _fetchAndCachePosts(true), // Mark as from polling
|
||||||
|
CacheService.postsPollingInterval,
|
||||||
|
CacheService.postsCacheDuration,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop background polling for posts
|
||||||
|
static void stopPolling() {
|
||||||
|
CacheService.stopPolling(CacheService.postsAllKey);
|
||||||
|
_pollingStarted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get posts stream for real-time updates
|
||||||
|
static Stream<List<Post>> getPostsStream() {
|
||||||
|
return CacheService.getStream<List<Post>>(CacheService.postsAllKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Force notify stream with current cached data (useful for initial stream setup)
|
||||||
|
static void notifyStreamWithCurrentData() {
|
||||||
|
final cached = CacheService.getCachedStale<List<Post>>(
|
||||||
|
CacheService.postsAllKey,
|
||||||
|
);
|
||||||
|
if (cached != null) {
|
||||||
|
// Re-notify the stream with current data
|
||||||
|
CacheService.notifyStreamListeners<List<Post>>(
|
||||||
|
CacheService.postsAllKey,
|
||||||
|
cached,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if polling is active
|
||||||
|
static bool get isPollingActive => _pollingStarted;
|
||||||
|
|
||||||
|
/// Invalidate posts cache (useful after creating/updating posts)
|
||||||
|
static void invalidateCache() {
|
||||||
|
CacheService.clearCache(CacheService.postsAllKey);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<Map<String, dynamic>> getUserPosts() async {
|
static Future<Map<String, dynamic>> getUserPosts() async {
|
||||||
try {
|
try {
|
||||||
final response = await HttpService.get('/posts/user');
|
final response = await HttpService.get('/posts/user');
|
||||||
@ -63,7 +157,11 @@ class PostService {
|
|||||||
return {
|
return {
|
||||||
'success': responseData['status'] ?? false,
|
'success': responseData['status'] ?? false,
|
||||||
'message': responseData['message'] ?? '',
|
'message': responseData['message'] ?? '',
|
||||||
'posts': (responseData['data'] as List?)?.map((post) => Post.fromJson(post)).toList() ?? [],
|
'posts':
|
||||||
|
(responseData['data'] as List?)
|
||||||
|
?.map((post) => Post.fromJson(post))
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
@ -93,6 +191,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
|
||||||
|
invalidateCache();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success': responseData['status'] ?? false,
|
'success': responseData['status'] ?? false,
|
||||||
'message': responseData['message'] ?? '',
|
'message': responseData['message'] ?? '',
|
||||||
@ -115,10 +216,9 @@ class PostService {
|
|||||||
|
|
||||||
static Future<Map<String, dynamic>> likePost(String postId) async {
|
static Future<Map<String, dynamic>> likePost(String postId) async {
|
||||||
try {
|
try {
|
||||||
final response = await HttpService.post(
|
final response = await HttpService.post('/posts/like', {
|
||||||
'/posts/like',
|
'postId': postId,
|
||||||
{'postId': postId},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
final responseData = jsonDecode(response.body);
|
final responseData = jsonDecode(response.body);
|
||||||
|
|
||||||
@ -145,10 +245,9 @@ class PostService {
|
|||||||
|
|
||||||
static Future<Map<String, dynamic>> unlikePost(String postId) async {
|
static Future<Map<String, dynamic>> unlikePost(String postId) async {
|
||||||
try {
|
try {
|
||||||
final response = await HttpService.post(
|
final response = await HttpService.post('/posts/unlike', {
|
||||||
'/posts/unlike',
|
'postId': postId,
|
||||||
{'postId': postId},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
final responseData = jsonDecode(response.body);
|
final responseData = jsonDecode(response.body);
|
||||||
|
|
||||||
@ -202,17 +301,16 @@ class PostService {
|
|||||||
|
|
||||||
static Future<Map<String, dynamic>> getComments(String postId) async {
|
static Future<Map<String, dynamic>> getComments(String postId) async {
|
||||||
try {
|
try {
|
||||||
final response = await HttpService.get('/posts/comments/list?postId=$postId');
|
final response = await HttpService.get(
|
||||||
|
'/posts/comments/list?postId=$postId',
|
||||||
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final responseData = jsonDecode(response.body);
|
final responseData = jsonDecode(response.body);
|
||||||
final commentsResponse = CommentsResponse.fromJson(responseData);
|
final commentsResponse = CommentsResponse.fromJson(responseData);
|
||||||
|
|
||||||
if (commentsResponse.status) {
|
if (commentsResponse.status) {
|
||||||
return {
|
return {'success': true, 'comments': commentsResponse.data};
|
||||||
'success': true,
|
|
||||||
'comments': commentsResponse.data,
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
'success': false,
|
'success': false,
|
||||||
@ -259,16 +357,16 @@ class PostService {
|
|||||||
String? replyComment,
|
String? replyComment,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final requestBody = {
|
final requestBody = {'postId': int.parse(postId), 'body': body};
|
||||||
'postId': int.parse(postId),
|
|
||||||
'body': body,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (replyComment != null) {
|
if (replyComment != null) {
|
||||||
requestBody['replyComment'] = int.parse(replyComment);
|
requestBody['replyComment'] = int.parse(replyComment);
|
||||||
}
|
}
|
||||||
|
|
||||||
final response = await HttpService.post('/posts/comments/create', requestBody);
|
final response = await HttpService.post(
|
||||||
|
'/posts/comments/create',
|
||||||
|
requestBody,
|
||||||
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
final responseData = jsonDecode(response.body);
|
final responseData = jsonDecode(response.body);
|
||||||
@ -306,10 +404,7 @@ class PostService {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error creating comment: $e');
|
print('Error creating comment: $e');
|
||||||
return {
|
return {'success': false, 'message': 'Network error: $e'};
|
||||||
'success': false,
|
|
||||||
'message': 'Network error: $e',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,35 +2,80 @@ import 'dart:convert';
|
|||||||
import '../constants/api_constants.dart';
|
import '../constants/api_constants.dart';
|
||||||
import 'http_service.dart';
|
import 'http_service.dart';
|
||||||
import 'auth_service.dart';
|
import 'auth_service.dart';
|
||||||
|
import 'cache_service.dart';
|
||||||
|
|
||||||
class UserService {
|
class UserService {
|
||||||
static Future<Map<String, dynamic>> getCurrentUser({
|
static bool _pollingStarted = false;
|
||||||
bool forceRefresh = false,
|
/// Get current user with caching - returns cached data if available and fresh
|
||||||
}) async {
|
static Future<Map<String, dynamic>> getCurrentUser({bool forceRefresh = false}) async {
|
||||||
|
// Return cached data if available and not forcing refresh
|
||||||
if (!forceRefresh) {
|
if (!forceRefresh) {
|
||||||
final cachedData = await AuthService.getCachedUserData();
|
final cached = CacheService.getCached<Map<String, dynamic>>(CacheService.userProfileKey);
|
||||||
if (cachedData != null) {
|
if (cached != null && !CacheService.isCacheExpired(CacheService.userProfileKey, CacheService.userCacheDuration)) {
|
||||||
return {'success': true, 'data': cachedData};
|
return {
|
||||||
|
'success': true,
|
||||||
|
'message': 'Loaded from cache',
|
||||||
|
'data': cached,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return stale data while fetching fresh data in background
|
||||||
|
final stale = CacheService.getCachedStale<Map<String, dynamic>>(CacheService.userProfileKey);
|
||||||
|
if (stale != null && !forceRefresh) {
|
||||||
|
// Trigger background refresh
|
||||||
|
_fetchAndCacheUser();
|
||||||
|
return {
|
||||||
|
'success': true,
|
||||||
|
'message': 'Loaded from cache (refreshing in background)',
|
||||||
|
'data': stale,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check AuthService cache for backward compatibility
|
||||||
|
final authCachedData = await AuthService.getCachedUserData();
|
||||||
|
if (authCachedData != null) {
|
||||||
|
// Update our cache with auth service data
|
||||||
|
CacheService.setCached(CacheService.userProfileKey, authCachedData);
|
||||||
|
return {'success': true, 'data': authCachedData};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch fresh data
|
||||||
|
return await _fetchAndCacheUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> _fetchAndCacheUser([bool fromPolling = false]) async {
|
||||||
try {
|
try {
|
||||||
final response = await HttpService.get(ApiConstants.getUserEndpoint);
|
final response = await HttpService.get(ApiConstants.getUserEndpoint);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
|
|
||||||
|
// Only cache when not called from polling to prevent conflicts
|
||||||
|
if (!fromPolling) {
|
||||||
|
CacheService.setCached(CacheService.userProfileKey, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also save to AuthService for backward compatibility
|
||||||
await AuthService.saveUserData(data);
|
await AuthService.saveUserData(data);
|
||||||
return {'success': true, 'data': data};
|
|
||||||
|
return {
|
||||||
|
'success': true,
|
||||||
|
'data': data,
|
||||||
|
'message': '',
|
||||||
|
};
|
||||||
} else if (response.statusCode == 401 || response.statusCode == 403) {
|
} else if (response.statusCode == 401 || response.statusCode == 403) {
|
||||||
await AuthService.handleAuthenticationError();
|
await AuthService.handleAuthenticationError();
|
||||||
return {
|
return {
|
||||||
'success': false,
|
'success': false,
|
||||||
'message': 'Session expired. Please login again.',
|
'message': 'Session expired. Please login again.',
|
||||||
|
'data': null,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
'success': false,
|
'success': false,
|
||||||
'message': 'Server error (${response.statusCode})',
|
'message': 'Server error (${response.statusCode})',
|
||||||
|
'data': null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -38,10 +83,41 @@ class UserService {
|
|||||||
return {
|
return {
|
||||||
'success': false,
|
'success': false,
|
||||||
'message': 'Network error. Please check your connection.',
|
'message': 'Network error. Please check your connection.',
|
||||||
|
'data': null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Start background polling for user profile
|
||||||
|
static void startPolling() {
|
||||||
|
_pollingStarted = true;
|
||||||
|
CacheService.startPolling(
|
||||||
|
CacheService.userProfileKey,
|
||||||
|
() => _fetchAndCacheUser(true), // Mark as from polling
|
||||||
|
CacheService.userPollingInterval,
|
||||||
|
CacheService.userCacheDuration,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop background polling for user profile
|
||||||
|
static void stopPolling() {
|
||||||
|
CacheService.stopPolling(CacheService.userProfileKey);
|
||||||
|
_pollingStarted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get user profile stream for real-time updates
|
||||||
|
static Stream<Map<String, dynamic>> getUserStream() {
|
||||||
|
return CacheService.getStream<Map<String, dynamic>>(CacheService.userProfileKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if polling is active
|
||||||
|
static bool get isPollingActive => _pollingStarted;
|
||||||
|
|
||||||
|
/// Invalidate user cache (useful after updating profile)
|
||||||
|
static void invalidateCache() {
|
||||||
|
CacheService.clearCache(CacheService.userProfileKey);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<Map<String, dynamic>> updateFCMToken(String fcmToken) async {
|
static Future<Map<String, dynamic>> updateFCMToken(String fcmToken) async {
|
||||||
try {
|
try {
|
||||||
final response = await HttpService.post(ApiConstants.updateUserEndpoint, {
|
final response = await HttpService.post(ApiConstants.updateUserEndpoint, {
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'dart:async';
|
||||||
import '../models/post_models.dart';
|
import '../models/post_models.dart';
|
||||||
import '../services/post_service.dart';
|
import '../services/post_service.dart';
|
||||||
import '../utils/invitation_utils.dart';
|
import '../utils/invitation_utils.dart';
|
||||||
import '../screens/pages/post_view_page.dart';
|
import '../screens/pages/post_view_page.dart';
|
||||||
|
|
||||||
class PostsList extends StatefulWidget {
|
class PostsList extends StatefulWidget {
|
||||||
final Future<Map<String, dynamic>> Function() fetchPosts;
|
final Future<Map<String, dynamic>> Function({bool forceRefresh}) fetchPosts;
|
||||||
final String emptyStateTitle;
|
final String emptyStateTitle;
|
||||||
final String emptyStateSubtitle;
|
final String emptyStateSubtitle;
|
||||||
final bool showRefreshIndicator;
|
final bool showRefreshIndicator;
|
||||||
@ -29,39 +30,97 @@ class PostsListState extends State<PostsList> {
|
|||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
List<Post> _posts = [];
|
List<Post> _posts = [];
|
||||||
String _errorMessage = '';
|
String _errorMessage = '';
|
||||||
|
StreamSubscription<List<Post>>? _postsStreamSubscription;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadPosts();
|
_setupPostsStream(); // This will also call _loadPosts()
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadPosts() async {
|
@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(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_errorMessage = '';
|
_errorMessage = '';
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final result = await widget.fetchPosts();
|
final result = await widget.fetchPosts(forceRefresh: forceRefresh);
|
||||||
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (result['success']) {
|
if (result['success']) {
|
||||||
_posts = result['posts'];
|
_posts = result['posts'];
|
||||||
|
_errorMessage = '';
|
||||||
} else {
|
} else {
|
||||||
_errorMessage = result['message'] ?? 'Failed to load posts';
|
_errorMessage = result['message'] ?? 'Failed to load posts';
|
||||||
}
|
}
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_errorMessage = 'Network error: $e';
|
_errorMessage = 'Network error: $e';
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _refreshPosts() async {
|
Future<void> _refreshPosts() async {
|
||||||
await _loadPosts();
|
await _loadPosts(forceRefresh: true);
|
||||||
|
|
||||||
if (widget.showRefreshIndicator) {
|
if (widget.showRefreshIndicator) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@ -154,7 +213,7 @@ class PostsListState extends State<PostsList> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void refreshPosts() {
|
void refreshPosts() {
|
||||||
_loadPosts();
|
_loadPosts(forceRefresh: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,9 +239,14 @@ class _PostCardState extends State<PostCard> {
|
|||||||
@override
|
@override
|
||||||
void didUpdateWidget(PostCard oldWidget) {
|
void didUpdateWidget(PostCard oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
// Update current post if the widget's post changed
|
// Force update current post to reflect any changes
|
||||||
if (oldWidget.post.id != widget.post.id) {
|
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;
|
_currentPost = widget.post;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -66,7 +66,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.4+2"
|
version: "0.3.4+2"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||||
|
|||||||
@ -16,6 +16,7 @@ dependencies:
|
|||||||
googleapis_auth: ^1.6.0
|
googleapis_auth: ^1.6.0
|
||||||
flutter_secure_storage: ^9.2.2
|
flutter_secure_storage: ^9.2.2
|
||||||
share_plus: ^11.0.0
|
share_plus: ^11.0.0
|
||||||
|
crypto: ^3.0.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user