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 '../../theme/app_theme.dart';
|
||||||
import '../../widgets/sojorn_post_card.dart';
|
import '../../widgets/sojorn_post_card.dart';
|
||||||
import '../../widgets/app_scaffold.dart';
|
import '../../widgets/app_scaffold.dart';
|
||||||
|
import '../../widgets/feed_filter_button.dart';
|
||||||
import '../compose/compose_screen.dart';
|
import '../compose/compose_screen.dart';
|
||||||
import '../post/post_detail_screen.dart';
|
import '../post/post_detail_screen.dart';
|
||||||
import '../../widgets/first_use_hint.dart';
|
import '../../widgets/first_use_hint.dart';
|
||||||
|
|
@ -54,6 +55,7 @@ class _FeedPersonalScreenState extends ConsumerState<FeedPersonalScreen> {
|
||||||
final posts = await apiService.getPersonalFeed(
|
final posts = await apiService.getPersonalFeed(
|
||||||
limit: 50,
|
limit: 50,
|
||||||
offset: refresh ? 0 : _posts.length,
|
offset: refresh ? 0 : _posts.length,
|
||||||
|
filterType: _currentFilter.typeValue,
|
||||||
);
|
);
|
||||||
|
|
||||||
_setStateIfMounted(() {
|
_setStateIfMounted(() {
|
||||||
|
|
@ -93,6 +95,11 @@ class _FeedPersonalScreenState extends ConsumerState<FeedPersonalScreen> {
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onFilterChanged(FeedFilter filter) {
|
||||||
|
setState(() => _currentFilter = filter);
|
||||||
|
_loadPosts(refresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
ref.listen<int>(feedRefreshProvider, (_, __) {
|
ref.listen<int>(feedRefreshProvider, (_, __) {
|
||||||
|
|
@ -102,6 +109,12 @@ class _FeedPersonalScreenState extends ConsumerState<FeedPersonalScreen> {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
title: '',
|
title: '',
|
||||||
showAppBar: false,
|
showAppBar: false,
|
||||||
|
actions: [
|
||||||
|
FeedFilterButton(
|
||||||
|
currentFilter: _currentFilter,
|
||||||
|
onFilterChanged: _onFilterChanged,
|
||||||
|
),
|
||||||
|
],
|
||||||
body: _error != null
|
body: _error != null
|
||||||
? _ErrorState(
|
? _ErrorState(
|
||||||
message: _error!,
|
message: _error!,
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import '../post/post_detail_screen.dart';
|
||||||
import 'profile_settings_screen.dart';
|
import 'profile_settings_screen.dart';
|
||||||
import 'followers_following_screen.dart';
|
import 'followers_following_screen.dart';
|
||||||
import '../../widgets/harmony_explainer_modal.dart';
|
import '../../widgets/harmony_explainer_modal.dart';
|
||||||
|
import '../../widgets/follow_button.dart';
|
||||||
|
|
||||||
/// Unified profile screen - handles both own profile and viewing others.
|
/// Unified profile screen - handles both own profile and viewing others.
|
||||||
///
|
///
|
||||||
|
|
@ -70,6 +71,8 @@ class _UnifiedProfileScreenState extends ConsumerState<UnifiedProfileScreen>
|
||||||
bool _isCreatingProfile = false;
|
bool _isCreatingProfile = false;
|
||||||
ProfilePrivacySettings? _privacySettings;
|
ProfilePrivacySettings? _privacySettings;
|
||||||
bool _isPrivacyLoading = false;
|
bool _isPrivacyLoading = false;
|
||||||
|
List<Map<String, dynamic>> _mutualFollowers = [];
|
||||||
|
bool _isMutualFollowersLoading = false;
|
||||||
|
|
||||||
/// True when no handle was provided (bottom-nav profile tab)
|
/// True when no handle was provided (bottom-nav profile tab)
|
||||||
bool get _isOwnProfileMode => widget.handle == null;
|
bool get _isOwnProfileMode => widget.handle == null;
|
||||||
|
|
|
||||||
|
|
@ -1360,14 +1360,30 @@ class ApiService {
|
||||||
// Notifications & Feed (Missing Methods)
|
// Notifications & Feed (Missing Methods)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
Future<List<Post>> getPersonalFeed({int limit = 20, int offset = 0}) async {
|
Future<List<Post>> getPersonalFeed({
|
||||||
final data = await callGoApi(
|
int limit = 20,
|
||||||
'/feed',
|
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',
|
method: 'GET',
|
||||||
queryParams: {'limit': '$limit', 'offset': '$offset'},
|
queryParams: queryParams,
|
||||||
);
|
);
|
||||||
final posts = data['posts'] as List? ?? [];
|
if (data['posts'] != null) {
|
||||||
return posts.map((p) => Post.fromJson(p)).toList();
|
return (data['posts'] as List)
|
||||||
|
.map((json) => Post.fromJson(json))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Post>> getSojornFeed({int limit = 20, int offset = 0}) async {
|
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