feat: Add feed filtering UI with FeedFilterButton and integrate into personal feed

This commit is contained in:
Patrick Britton 2026-02-17 10:33:32 -06:00
parent 62233d5892
commit 7f618bcdf2
4 changed files with 90 additions and 6 deletions

View file

@ -7,6 +7,7 @@ import '../../models/feed_filter.dart';
import '../../theme/app_theme.dart';
import '../../widgets/sojorn_post_card.dart';
import '../../widgets/app_scaffold.dart';
import '../../widgets/feed_filter_button.dart';
import '../compose/compose_screen.dart';
import '../post/post_detail_screen.dart';
import '../../widgets/first_use_hint.dart';
@ -54,6 +55,7 @@ class _FeedPersonalScreenState extends ConsumerState<FeedPersonalScreen> {
final posts = await apiService.getPersonalFeed(
limit: 50,
offset: refresh ? 0 : _posts.length,
filterType: _currentFilter.typeValue,
);
_setStateIfMounted(() {
@ -93,6 +95,11 @@ class _FeedPersonalScreenState extends ConsumerState<FeedPersonalScreen> {
FocusManager.instance.primaryFocus?.unfocus();
}
void _onFilterChanged(FeedFilter filter) {
setState(() => _currentFilter = filter);
_loadPosts(refresh: true);
}
@override
Widget build(BuildContext context) {
ref.listen<int>(feedRefreshProvider, (_, __) {
@ -102,6 +109,12 @@ class _FeedPersonalScreenState extends ConsumerState<FeedPersonalScreen> {
return AppScaffold(
title: '',
showAppBar: false,
actions: [
FeedFilterButton(
currentFilter: _currentFilter,
onFilterChanged: _onFilterChanged,
),
],
body: _error != null
? _ErrorState(
message: _error!,

View file

@ -25,6 +25,7 @@ import '../post/post_detail_screen.dart';
import 'profile_settings_screen.dart';
import 'followers_following_screen.dart';
import '../../widgets/harmony_explainer_modal.dart';
import '../../widgets/follow_button.dart';
/// Unified profile screen - handles both own profile and viewing others.
///
@ -70,6 +71,8 @@ class _UnifiedProfileScreenState extends ConsumerState<UnifiedProfileScreen>
bool _isCreatingProfile = false;
ProfilePrivacySettings? _privacySettings;
bool _isPrivacyLoading = false;
List<Map<String, dynamic>> _mutualFollowers = [];
bool _isMutualFollowersLoading = false;
/// True when no handle was provided (bottom-nav profile tab)
bool get _isOwnProfileMode => widget.handle == null;

View file

@ -1360,14 +1360,30 @@ class ApiService {
// Notifications & Feed (Missing Methods)
// =========================================================================
Future<List<Post>> getPersonalFeed({int limit = 20, int offset = 0}) async {
final data = await callGoApi(
'/feed',
Future<List<Post>> getPersonalFeed({
int limit = 20,
int offset = 0,
String? filterType,
}) async {
final queryParams = {
'limit': '$limit',
'offset': '$offset',
};
if (filterType != null) {
queryParams['type'] = filterType;
}
final data = await _callGoApi(
'/feed/personal',
method: 'GET',
queryParams: {'limit': '$limit', 'offset': '$offset'},
queryParams: queryParams,
);
final posts = data['posts'] as List? ?? [];
return posts.map((p) => Post.fromJson(p)).toList();
if (data['posts'] != null) {
return (data['posts'] as List)
.map((json) => Post.fromJson(json))
.toList();
}
return [];
}
Future<List<Post>> getSojornFeed({int limit = 20, int offset = 0}) async {

View file

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import '../models/feed_filter.dart';
import '../theme/app_theme.dart';
/// Filter button for feed screens with popup menu
class FeedFilterButton extends StatelessWidget {
final FeedFilter currentFilter;
final ValueChanged<FeedFilter> onFilterChanged;
const FeedFilterButton({
super.key,
required this.currentFilter,
required this.onFilterChanged,
});
@override
Widget build(BuildContext context) {
return PopupMenuButton<FeedFilter>(
icon: Icon(
Icons.filter_list,
color: currentFilter != FeedFilter.all ? AppTheme.navyBlue : null,
),
initialValue: currentFilter,
onSelected: onFilterChanged,
tooltip: 'Filter posts',
itemBuilder: (context) => [
_buildMenuItem(FeedFilter.all, Icons.apps),
_buildMenuItem(FeedFilter.posts, Icons.article_outlined),
_buildMenuItem(FeedFilter.quips, Icons.play_circle_outline),
_buildMenuItem(FeedFilter.chains, Icons.forum_outlined),
_buildMenuItem(FeedFilter.beacons, Icons.sensors),
],
);
}
PopupMenuItem<FeedFilter> _buildMenuItem(FeedFilter filter, IconData icon) {
return PopupMenuItem(
value: filter,
child: Row(
children: [
Icon(icon, size: 20),
const SizedBox(width: 12),
Text(filter.label),
if (filter == currentFilter) ...[
const Spacer(),
Icon(Icons.check, size: 18, color: AppTheme.navyBlue),
],
],
),
);
}
}