diff --git a/sojorn_app/lib/screens/home/feed_personal_screen.dart b/sojorn_app/lib/screens/home/feed_personal_screen.dart index 014266c..cc681ea 100644 --- a/sojorn_app/lib/screens/home/feed_personal_screen.dart +++ b/sojorn_app/lib/screens/home/feed_personal_screen.dart @@ -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 { final posts = await apiService.getPersonalFeed( limit: 50, offset: refresh ? 0 : _posts.length, + filterType: _currentFilter.typeValue, ); _setStateIfMounted(() { @@ -93,6 +95,11 @@ class _FeedPersonalScreenState extends ConsumerState { FocusManager.instance.primaryFocus?.unfocus(); } + void _onFilterChanged(FeedFilter filter) { + setState(() => _currentFilter = filter); + _loadPosts(refresh: true); + } + @override Widget build(BuildContext context) { ref.listen(feedRefreshProvider, (_, __) { @@ -102,6 +109,12 @@ class _FeedPersonalScreenState extends ConsumerState { return AppScaffold( title: '', showAppBar: false, + actions: [ + FeedFilterButton( + currentFilter: _currentFilter, + onFilterChanged: _onFilterChanged, + ), + ], body: _error != null ? _ErrorState( message: _error!, diff --git a/sojorn_app/lib/screens/profile/viewable_profile_screen.dart b/sojorn_app/lib/screens/profile/viewable_profile_screen.dart index 0f694d5..3abae25 100644 --- a/sojorn_app/lib/screens/profile/viewable_profile_screen.dart +++ b/sojorn_app/lib/screens/profile/viewable_profile_screen.dart @@ -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 bool _isCreatingProfile = false; ProfilePrivacySettings? _privacySettings; bool _isPrivacyLoading = false; + List> _mutualFollowers = []; + bool _isMutualFollowersLoading = false; /// True when no handle was provided (bottom-nav profile tab) bool get _isOwnProfileMode => widget.handle == null; diff --git a/sojorn_app/lib/services/api_service.dart b/sojorn_app/lib/services/api_service.dart index a266a65..1e24f45 100644 --- a/sojorn_app/lib/services/api_service.dart +++ b/sojorn_app/lib/services/api_service.dart @@ -1360,14 +1360,30 @@ class ApiService { // Notifications & Feed (Missing Methods) // ========================================================================= - Future> getPersonalFeed({int limit = 20, int offset = 0}) async { - final data = await callGoApi( - '/feed', + Future> 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> getSojornFeed({int limit = 20, int offset = 0}) async { diff --git a/sojorn_app/lib/widgets/feed_filter_button.dart b/sojorn_app/lib/widgets/feed_filter_button.dart new file mode 100644 index 0000000..4043c3b --- /dev/null +++ b/sojorn_app/lib/widgets/feed_filter_button.dart @@ -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 onFilterChanged; + + const FeedFilterButton({ + super.key, + required this.currentFilter, + required this.onFilterChanged, + }); + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + 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 _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), + ], + ], + ), + ); + } +}