diff --git a/lib/src/features/feed/ui/pages/feeds_page.dart b/lib/src/features/feed/ui/pages/feeds_page.dart index 27443a2a..8d8ec26d 100644 --- a/lib/src/features/feed/ui/pages/feeds_page.dart +++ b/lib/src/features/feed/ui/pages/feeds_page.dart @@ -59,7 +59,17 @@ class _FeedsPageState extends ConsumerState { } if (_pageController == null) return; - if (!_pageController!.hasClients) return; + if (!_pageController!.hasClients) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || + _pageController == null || + !_pageController!.hasClients) { + return; + } + _updatePageController(feeds, activeFeed, forceJump: forceJump); + }); + return; + } final currentPage = _pageController!.page?.round() ?? 0; if ((currentPage != activeIndex || forceJump) && activeIndex >= 0) { @@ -124,9 +134,11 @@ class _FeedsPageState extends ConsumerState { if (needsInitialization || activeFeedChanged || feedsOrderChanged) { // Force jump when order changes to ensure we stay on the active feed _updatePageController(feeds, activeFeed, forceJump: feedsOrderChanged); - _isInitialized = true; - _lastActiveFeed = activeFeed; - _lastFeedsList = List.from(feeds); // Create a copy + if (_pageController != null && _pageController!.hasClients) { + _isInitialized = true; + _lastActiveFeed = activeFeed; + _lastFeedsList = List.from(feeds); // Create a copy + } } // Ensure controller is created if we have feeds but controller is null diff --git a/lib/src/features/feed/ui/widgets/videos/video_player.dart b/lib/src/features/feed/ui/widgets/videos/video_player.dart index 01c7795a..2bcfc848 100644 --- a/lib/src/features/feed/ui/widgets/videos/video_player.dart +++ b/lib/src/features/feed/ui/widgets/videos/video_player.dart @@ -282,7 +282,7 @@ class PostVideoPlayerState extends ConsumerState } else if (profileFeedIndex != null && widget.index != null) { // Profile feed visibility check if (profileFeedIndex == -1) { - // Provider not initialized yet - use isInitialPost for initial autoplay + // Provider not initialized, use isInitialPost for initial autoplay if (widget.isInitialPost && _lastFeedIndex == null) { _lastFeedIndex = -1; // Mark as handled WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/lib/src/features/notifications/ui/pages/notifications_page.dart b/lib/src/features/notifications/ui/pages/notifications_page.dart index f0ea99dd..315dfc84 100644 --- a/lib/src/features/notifications/ui/pages/notifications_page.dart +++ b/lib/src/features/notifications/ui/pages/notifications_page.dart @@ -1,7 +1,6 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spark/src/core/ui/foundation/colors.dart'; import 'package:spark/src/features/notifications/providers/notification_provider.dart' show notificationProvider; import 'package:spark/src/features/notifications/ui/widgets/notifications_list.dart'; diff --git a/lib/src/features/profile/providers/profile_feed_provider.dart b/lib/src/features/profile/providers/profile_feed_provider.dart index 9f0098dc..f2341b63 100644 --- a/lib/src/features/profile/providers/profile_feed_provider.dart +++ b/lib/src/features/profile/providers/profile_feed_provider.dart @@ -98,8 +98,7 @@ class ProfileFeed extends _$ProfileFeed { // 2. API returned posts but all were duplicates (prevents infinite loops) // Note: Don't check posts.isEmpty - empty page with cursor means more exist final isEndOfNetwork = - result.cursor == null || - (result.posts.isNotEmpty && newPosts.isEmpty); + result.cursor == null || (result.posts.isNotEmpty && newPosts.isEmpty); return ProfileFeedState( loadedPosts: allPosts, diff --git a/lib/src/features/profile/providers/profile_likes_provider.dart b/lib/src/features/profile/providers/profile_likes_provider.dart index c73c5d70..9854a8f0 100644 --- a/lib/src/features/profile/providers/profile_likes_provider.dart +++ b/lib/src/features/profile/providers/profile_likes_provider.dart @@ -92,8 +92,7 @@ class ProfileLikes extends _$ProfileLikes { // 2. API returned posts but all were duplicates (prevents infinite loops) // Note: Don't check posts.isEmpty - empty page with cursor means more exist final isEndOfNetwork = - result.cursor == null || - (result.posts.isNotEmpty && newPosts.isEmpty); + result.cursor == null || (result.posts.isNotEmpty && newPosts.isEmpty); return ProfileFeedState( loadedPosts: allPosts, diff --git a/lib/src/features/profile/providers/profile_reposts_provider.dart b/lib/src/features/profile/providers/profile_reposts_provider.dart index 7d625634..e3a1c20d 100644 --- a/lib/src/features/profile/providers/profile_reposts_provider.dart +++ b/lib/src/features/profile/providers/profile_reposts_provider.dart @@ -92,8 +92,7 @@ class ProfileReposts extends _$ProfileReposts { // 2. API returned posts but all were duplicates (prevents infinite loops) // Note: Don't check posts.isEmpty - empty page with cursor means more exist final isEndOfNetwork = - result.cursor == null || - (result.posts.isNotEmpty && newPosts.isEmpty); + result.cursor == null || (result.posts.isNotEmpty && newPosts.isEmpty); return ProfileFeedState( loadedPosts: allPosts, diff --git a/lib/src/features/profile/ui/widgets/profile_grid_widget.dart b/lib/src/features/profile/ui/widgets/profile_grid_widget.dart index 66b4ef89..738bb236 100644 --- a/lib/src/features/profile/ui/widgets/profile_grid_widget.dart +++ b/lib/src/features/profile/ui/widgets/profile_grid_widget.dart @@ -60,8 +60,8 @@ List buildProfileGridSlivers({ } // Add bottom padding to account for tab bar when on main navigation - final bottomPadding = MediaQuery.of(context).padding.bottom + - kBottomNavigationBarHeight; + final bottomPadding = + MediaQuery.of(context).padding.bottom + kBottomNavigationBarHeight; return [ SliverPadding( diff --git a/lib/src/features/profile/ui/widgets/profile_likes_tab.dart b/lib/src/features/profile/ui/widgets/profile_likes_tab.dart index 2a0f729a..906cbaca 100644 --- a/lib/src/features/profile/ui/widgets/profile_likes_tab.dart +++ b/lib/src/features/profile/ui/widgets/profile_likes_tab.dart @@ -102,8 +102,8 @@ class ProfileLikesTab extends ProfileTabBase { } // Add bottom padding to account for tab bar when on main navigation - final bottomPadding = MediaQuery.of(context).padding.bottom + - kBottomNavigationBarHeight; + final bottomPadding = + MediaQuery.of(context).padding.bottom + kBottomNavigationBarHeight; return [ SliverPadding( diff --git a/lib/src/features/profile/ui/widgets/profile_reposts_tab.dart b/lib/src/features/profile/ui/widgets/profile_reposts_tab.dart index 70e6fdfb..6ffc4c15 100644 --- a/lib/src/features/profile/ui/widgets/profile_reposts_tab.dart +++ b/lib/src/features/profile/ui/widgets/profile_reposts_tab.dart @@ -102,8 +102,8 @@ class ProfileRepostsTab extends ProfileTabBase { } // Add bottom padding to account for tab bar when on main navigation - final bottomPadding = MediaQuery.of(context).padding.bottom + - kBottomNavigationBarHeight; + final bottomPadding = + MediaQuery.of(context).padding.bottom + kBottomNavigationBarHeight; return [ SliverPadding( diff --git a/lib/src/features/settings/providers/settings_provider.dart b/lib/src/features/settings/providers/settings_provider.dart index 30c4f3ce..956445ff 100644 --- a/lib/src/features/settings/providers/settings_provider.dart +++ b/lib/src/features/settings/providers/settings_provider.dart @@ -8,6 +8,7 @@ import 'package:spark/src/core/network/atproto/data/repositories/feed_repository import 'package:spark/src/core/network/atproto/data/repositories/pref_repository.dart'; import 'package:spark/src/core/network/atproto/data/repositories/sprk_repository.dart'; import 'package:spark/src/core/storage/preferences/default_preferences.dart'; +import 'package:spark/src/core/storage/preferences/storage_manager.dart'; import 'package:spark/src/core/utils/logging/log_service.dart'; import 'package:spark/src/core/utils/logging/logger.dart'; import 'package:spark/src/features/settings/providers/preferences_provider.dart'; @@ -55,6 +56,12 @@ class Settings extends _$Settings { config: SavedFeed(type: 'timeline', value: 'following', pinned: true), ); + /// Storage key for the last active feed, unique per user (DID) + String get _activeFeedStorageKey { + final did = sprkRepository.authRepository.did ?? 'anonymous'; + return 'active_feed_$did'; + } + String get _defaultModServiceDid { // Extract DID part from modDid (remove fragment if present) final modDid = sprkRepository.modDid; @@ -84,6 +91,36 @@ class Settings extends _$Settings { ); } + /// Loads the last active feed from local storage + /// Returns null if no feed is saved or if loading fails + Future _loadLastActiveFeedFromStorage() async { + try { + final storage = GetIt.instance().preferences; + final json = await storage.getObject>( + _activeFeedStorageKey, + ); + if (json != null) { + final feed = Feed.fromJson(json); + logger.d('Loaded saved active feed: ${feed.config.value}'); + return feed; + } + } catch (e) { + logger.w('Error loading saved active feed: $e'); + } + return null; + } + + /// Saves the active feed to local storage + Future _saveActiveFeedToStorage(Feed feed) async { + try { + final storage = GetIt.instance().preferences; + await storage.setObject(_activeFeedStorageKey, feed.toJson()); + logger.d('Saved active feed to storage: ${feed.config.value}'); + } catch (e) { + logger.w('Error saving active feed: $e'); + } + } + @override SettingsState build() { // Note: We intentionally don't watch userPreferencesProvider here. @@ -180,6 +217,9 @@ class Settings extends _$Settings { likedFeeds: likedFeeds, ); _hasLoadedSettings = true; + + // Save the default active feed to storage + await _saveActiveFeedToStorage(updatedActiveFeed); return; } catch (e) { logger.e('Error setting default preferences: $e'); @@ -189,7 +229,25 @@ class Settings extends _$Settings { // Hydrate feeds with generator views using getFeedGenerators final feeds = await feedRepository.getFeedsFromSavedFeeds(savedFeeds); - final activeFeed = _getActiveFeedFromFeeds(feeds, savedFeeds); + + // Try to load the last active feed from local storage + final savedActiveFeed = await _loadLastActiveFeedFromStorage(); + + // Determine active feed: use saved feed if it still exists in feeds list, + // otherwise fall back to server preferences (first pinned) + final Feed activeFeed; + if (savedActiveFeed != null && + savedActiveFeed.config.pinned && + feeds.any((f) => f.config.id == savedActiveFeed.config.id)) { + activeFeed = feeds.firstWhere( + (f) => f.config.id == savedActiveFeed.config.id, + ); + logger.d( + 'Restored last active feed from storage: ${activeFeed.config.value}', + ); + } else { + activeFeed = _getActiveFeedFromFeeds(feeds, savedFeeds); + } logger.d( 'Settings loaded - activeFeed: ${activeFeed.config.value}, ' @@ -375,9 +433,10 @@ class Settings extends _$Settings { await _updateFeedsInPreferences(updatedList); } - /// Sets selected feed index + /// Sets selected feed index and saves to local storage Future setActiveFeed(Feed feed) async { state = state.copyWith(activeFeed: feed); + await _saveActiveFeedToStorage(feed); } /// Debug method to reload settings and verify persistence