feat: Add feed filtering UI with FeedFilterButton and integrate into personal feed
This commit is contained in:
parent
62233d5892
commit
7f618bcdf2
|
|
@ -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!,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
52
sojorn_app/lib/widgets/feed_filter_button.dart
Normal file
52
sojorn_app/lib/widgets/feed_filter_button.dart
Normal 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),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue