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 269 newPosts = [...currentState.posts, ...response.feed]; 270 270 } 271 271 272 + final hasMore = response.cursor != null; 272 273 _feedStates[type] = currentState.copyWith( 273 274 posts: newPosts, 274 275 cursor: response.cursor, 275 - hasMore: response.cursor != null, 276 + hasMore: hasMore, 276 277 error: null, 277 278 isLoading: false, 278 279 isLoadingMore: false,
+1
lib/screens/home/feed_screen.dart
··· 376 376 posts: state.posts, 377 377 isLoading: state.isLoading, 378 378 isLoadingMore: state.isLoadingMore, 379 + hasMore: state.hasMore, 379 380 error: hasError ? error : null, 380 381 scrollController: _getOrCreateScrollController(feedType), 381 382 onRefresh: () => provider.loadFeed(feedType, refresh: true),
+33 -3
lib/widgets/feed_page.dart
··· 25 25 required this.posts, 26 26 required this.isLoading, 27 27 required this.isLoadingMore, 28 + required this.hasMore, 28 29 required this.error, 29 30 required this.scrollController, 30 31 required this.onRefresh, ··· 39 40 final List<FeedViewPost> posts; 40 41 final bool isLoading; 41 42 final bool isLoadingMore; 43 + final bool hasMore; 42 44 final String? error; 43 45 final ScrollController scrollController; 44 46 final Future<void> Function() onRefresh; ··· 55 57 with AutomaticKeepAliveClientMixin { 56 58 @override 57 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; 58 64 59 65 @override 60 66 Widget build(BuildContext context) { ··· 185 191 cacheExtent: 800, 186 192 // Add top padding so content isn't hidden behind transparent header 187 193 padding: const EdgeInsets.only(top: 44), 188 - // Add extra item for loading indicator or pagination error 194 + // Add extra item for loading indicator, pagination error, or end of feed 189 195 itemCount: 190 196 widget.posts.length + 191 - (widget.isLoadingMore || widget.error != null ? 1 : 0), 197 + (_shouldShowFooter ? 1 : 0), 192 198 itemBuilder: (context, index) { 193 - // Footer: loading indicator or error message 199 + // Footer: loading indicator, error message, or end of feed 194 200 if (index == widget.posts.length) { 195 201 // Show loading indicator for pagination 196 202 if (widget.isLoadingMore) { ··· 234 240 foregroundColor: AppColors.primary, 235 241 ), 236 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 + ), 237 267 ), 238 268 ], 239 269 ),