feat(feed): add end-of-feed indicator and pass hasMore to FeedPage

- Add hasMore prop to FeedPage widget to track pagination state
- Show "You're all caught up!" message with checkmark when feed ends
- Add _shouldShowFooter getter for cleaner footer logic
- Prevents confusing bounce behavior when reaching end of feed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Changed files
+36 -4
lib
+2 -1
lib/providers/multi_feed_provider.dart
··· 269 newPosts = [...currentState.posts, ...response.feed]; 270 } 271 272 _feedStates[type] = currentState.copyWith( 273 posts: newPosts, 274 cursor: response.cursor, 275 - hasMore: response.cursor != null, 276 error: null, 277 isLoading: false, 278 isLoadingMore: false,
··· 269 newPosts = [...currentState.posts, ...response.feed]; 270 } 271 272 + final hasMore = response.cursor != null; 273 _feedStates[type] = currentState.copyWith( 274 posts: newPosts, 275 cursor: response.cursor, 276 + hasMore: hasMore, 277 error: null, 278 isLoading: false, 279 isLoadingMore: false,
+1
lib/screens/home/feed_screen.dart
··· 376 posts: state.posts, 377 isLoading: state.isLoading, 378 isLoadingMore: state.isLoadingMore, 379 error: hasError ? error : null, 380 scrollController: _getOrCreateScrollController(feedType), 381 onRefresh: () => provider.loadFeed(feedType, refresh: true),
··· 376 posts: state.posts, 377 isLoading: state.isLoading, 378 isLoadingMore: state.isLoadingMore, 379 + hasMore: state.hasMore, 380 error: hasError ? error : null, 381 scrollController: _getOrCreateScrollController(feedType), 382 onRefresh: () => provider.loadFeed(feedType, refresh: true),
+33 -3
lib/widgets/feed_page.dart
··· 25 required this.posts, 26 required this.isLoading, 27 required this.isLoadingMore, 28 required this.error, 29 required this.scrollController, 30 required this.onRefresh, ··· 39 final List<FeedViewPost> posts; 40 final bool isLoading; 41 final bool isLoadingMore; 42 final String? error; 43 final ScrollController scrollController; 44 final Future<void> Function() onRefresh; ··· 55 with AutomaticKeepAliveClientMixin { 56 @override 57 bool get wantKeepAlive => true; 58 59 @override 60 Widget build(BuildContext context) { ··· 185 cacheExtent: 800, 186 // Add top padding so content isn't hidden behind transparent header 187 padding: const EdgeInsets.only(top: 44), 188 - // Add extra item for loading indicator or pagination error 189 itemCount: 190 widget.posts.length + 191 - (widget.isLoadingMore || widget.error != null ? 1 : 0), 192 itemBuilder: (context, index) { 193 - // Footer: loading indicator or error message 194 if (index == widget.posts.length) { 195 // Show loading indicator for pagination 196 if (widget.isLoadingMore) { ··· 234 foregroundColor: AppColors.primary, 235 ), 236 child: const Text('Retry'), 237 ), 238 ], 239 ),
··· 25 required this.posts, 26 required this.isLoading, 27 required this.isLoadingMore, 28 + required this.hasMore, 29 required this.error, 30 required this.scrollController, 31 required this.onRefresh, ··· 40 final List<FeedViewPost> posts; 41 final bool isLoading; 42 final bool isLoadingMore; 43 + final bool hasMore; 44 final String? error; 45 final ScrollController scrollController; 46 final Future<void> Function() onRefresh; ··· 57 with AutomaticKeepAliveClientMixin { 58 @override 59 bool get wantKeepAlive => true; 60 + 61 + /// Whether to show a footer item (loading, error, or end of feed) 62 + bool get _shouldShowFooter => 63 + widget.isLoadingMore || widget.error != null || !widget.hasMore; 64 65 @override 66 Widget build(BuildContext context) { ··· 191 cacheExtent: 800, 192 // Add top padding so content isn't hidden behind transparent header 193 padding: const EdgeInsets.only(top: 44), 194 + // Add extra item for loading indicator, pagination error, or end of feed 195 itemCount: 196 widget.posts.length + 197 + (_shouldShowFooter ? 1 : 0), 198 itemBuilder: (context, index) { 199 + // Footer: loading indicator, error message, or end of feed 200 if (index == widget.posts.length) { 201 // Show loading indicator for pagination 202 if (widget.isLoadingMore) { ··· 240 foregroundColor: AppColors.primary, 241 ), 242 child: const Text('Retry'), 243 + ), 244 + ], 245 + ), 246 + ); 247 + } 248 + // Show end of feed message when no more posts available 249 + if (!widget.hasMore) { 250 + return const Padding( 251 + padding: EdgeInsets.symmetric(vertical: 32, horizontal: 16), 252 + child: Column( 253 + children: [ 254 + Icon( 255 + Icons.check_circle_outline, 256 + color: AppColors.textSecondary, 257 + size: 32, 258 + ), 259 + SizedBox(height: 8), 260 + Text( 261 + "You're all caught up!", 262 + style: TextStyle( 263 + color: AppColors.textSecondary, 264 + fontSize: 14, 265 + fontWeight: FontWeight.w500, 266 + ), 267 ), 268 ], 269 ),