fix(social): resolve community feed discoverability, tab routing, live comments, and responsive overflows

This commit is contained in:
Developer 2026-09-30 09:49:02 -04:00
parent 71cd994b2f
commit d378db4c86
11 changed files with 606 additions and 253 deletions

View File

@ -139,7 +139,13 @@ final appRouter = GoRouter(
routes: [
GoRoute(
path: '/social',
builder: (context, state) => const SocialHubScreen(),
builder: (context, state) {
final tabParam = state.uri.queryParameters['tab'];
final initialTab = tabParam == 'hub' || tabParam == 'network'
? SocialHubTab.hub
: SocialHubTab.feed;
return SocialHubScreen(initialTab: initialTab);
},
routes: [
GoRoute(
path: 'profile',
@ -293,5 +299,13 @@ final appRouter = GoRouter(
path: '/launch',
builder: (context, state) => const PerformanceLaunchScreen(),
),
GoRoute(
path: '/social/feed',
builder: (context, state) => const SocialFeedScreen(),
),
GoRoute(
path: '/feed',
builder: (context, state) => const SocialFeedScreen(),
),
],
);

View File

@ -20,6 +20,7 @@ import 'ride_action_button.dart';
import 'ride_status_chip.dart';
import 'speed_readout.dart';
import '../../../services/alerts/widgets/quick_swarm_report_modal.dart';
import '../../social/feed/widgets/quick_squad_feed_button.dart';
/// The single authoritative responsive layout for the MotorSignal dashboard.
///
@ -82,6 +83,8 @@ class ResponsiveDashboardLayout extends ConsumerWidget {
SizedBox(width: 4),
QuickSwarmReportButton(compact: true),
SizedBox(width: 4),
QuickSquadFeedButton(compact: true),
SizedBox(width: 4),
GPSStatusIndicator(),
SizedBox(width: 8),
HeadingIndicator(),

View File

@ -18,13 +18,76 @@ import 'widgets/share_session_modal.dart';
/// snapshot cards, custom route vector track visualization, apex angle markers,
/// glove-friendly fist bump kudos reactions, comment drawers, and session publishing.
class SocialFeedScreen extends ConsumerWidget {
const SocialFeedScreen({super.key});
final bool showAppBar;
const SocialFeedScreen({super.key, this.showAppBar = true});
@override
Widget build(BuildContext context, WidgetRef ref) {
final colors = context.motorColors;
final activeFilter = ref.watch(feedFilterStateProvider);
final feedAsync = ref.watch(socialFeedStreamProvider);
final items = ref.watch(socialFeedNotifierProvider);
final content = SafeArea(
child: ResponsiveLayout(
builder: (context, responsive) {
return Stack(
children: [
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 720),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Top Context Header & Filter Bar
_buildTopFilterSection(context, ref, activeFilter, colors),
// Feed List
Expanded(
child: items.isEmpty
? _buildEmptyFeedState(context, activeFilter, colors)
: ListView.separated(
key: const ValueKey('social_feed_list_view'),
padding: const EdgeInsets.fromLTRB(16, 12, 16, 100),
itemCount: items.length,
separatorBuilder: (_, _) => const SizedBox(height: 16),
itemBuilder: (context, index) {
return _SocialFeedCard(
key: ValueKey('feed_card_${items[index].id}'),
item: items[index],
);
},
),
),
],
),
),
),
// Sticky Glove-Friendly Share Session Action Bar
Positioned(
left: 16,
right: 16,
bottom: 12,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 720),
child: _buildStickyShareButton(context),
),
),
),
],
);
},
),
);
if (!showAppBar) {
return Container(
color: const Color(0xFF101419),
child: content,
);
}
return Scaffold(
backgroundColor: const Color(0xFF101419),
@ -93,81 +156,14 @@ class SocialFeedScreen extends ConsumerWidget {
icon: const Icon(Icons.refresh_rounded, color: Color(0xFF849396)),
tooltip: 'Refresh Feed',
onPressed: () {
ref.invalidate(socialFeedNotifierProvider);
ref.invalidate(socialFeedStreamProvider);
},
),
const SizedBox(width: 4),
],
),
body: SafeArea(
child: ResponsiveLayout(
builder: (context, responsive) {
return Stack(
children: [
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 720),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Top Context Header & Filter Bar
_buildTopFilterSection(context, ref, activeFilter, colors),
// Feed List
Expanded(
child: feedAsync.when(
data: (items) {
if (items.isEmpty) {
return _buildEmptyFeedState(context, activeFilter, colors);
}
return ListView.separated(
key: const ValueKey('social_feed_list_view'),
padding: const EdgeInsets.fromLTRB(16, 12, 16, 80),
itemCount: items.length,
separatorBuilder: (_, _) => const SizedBox(height: 16),
itemBuilder: (context, index) {
return _SocialFeedCard(
key: ValueKey('feed_card_${items[index].id}'),
item: items[index],
);
},
);
},
loading: () => const Center(
child: CircularProgressIndicator(
color: AppColors.electricCyan,
),
),
error: (err, _) => Center(
child: Text(
'Error loading feed: $err',
style: const TextStyle(color: Colors.redAccent),
),
),
),
),
],
),
),
),
// Sticky Glove-Friendly Share Session Action Bar
Positioned(
left: 16,
right: 16,
bottom: 12,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 720),
child: _buildStickyShareButton(context),
),
),
),
],
);
},
),
),
body: content,
);
}
@ -869,15 +865,18 @@ class _SocialFeedCardState extends ConsumerState<_SocialFeedCard>
children: [
const Text('👊', style: TextStyle(fontSize: 18)),
const SizedBox(width: 8),
Text(
'$bumpCount Bumps',
style: TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 13,
fontWeight: FontWeight.w700,
color: isBumped
? const Color(0xFF00363D)
: Colors.white,
Flexible(
child: Text(
'$bumpCount Bumps',
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 13,
fontWeight: FontWeight.w700,
color: isBumped
? const Color(0xFF00363D)
: Colors.white,
),
),
),
],
@ -991,14 +990,17 @@ class _SocialFeedCardState extends ConsumerState<_SocialFeedCard>
color: AppColors.electricCyan,
),
SizedBox(width: 8),
Text(
'INSPECT TELEMETRY GRAPH',
style: TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 0.8,
color: AppColors.electricCyan,
Flexible(
child: Text(
'INSPECT TELEMETRY GRAPH',
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 0.8,
color: AppColors.electricCyan,
),
),
),
],

View File

@ -35,6 +35,13 @@ class _FeedCommentsBottomSheetState
extends ConsumerState<FeedCommentsBottomSheet> {
final TextEditingController _commentController = TextEditingController();
bool _isSubmitting = false;
late List<FeedPostComment> _localComments;
@override
void initState() {
super.initState();
_localComments = List<FeedPostComment>.from(widget.feedItem.comments);
}
@override
void dispose() {
@ -78,6 +85,9 @@ class _FeedCommentsBottomSheetState
_commentController.clear();
if (mounted) {
setState(() {
_localComments.add(comment);
});
FocusScope.of(context).unfocus();
}
} finally {
@ -91,7 +101,14 @@ class _FeedCommentsBottomSheetState
Widget build(BuildContext context) {
final colors = context.motorColors;
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
final comments = widget.feedItem.comments;
final feedItems = ref.watch(socialFeedNotifierProvider);
final livePost = feedItems.cast<SocialFeedItem?>().firstWhere(
(p) => p?.id == widget.feedItem.id,
orElse: () => null,
);
final comments = livePost != null && livePost.comments.isNotEmpty
? livePost.comments
: _localComments;
return Container(
decoration: BoxDecoration(

View File

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/theme/app_typography.dart';
/// Glove-friendly quick launcher button for the Squad & Canyon Community Feed.
///
/// Designed to sit directly in the Dashboard status bar for instantaneous
/// 1-tap access to live rider activity, route snapshots, and kudos.
class QuickSquadFeedButton extends StatelessWidget {
final bool compact;
const QuickSquadFeedButton({super.key, this.compact = false});
@override
Widget build(BuildContext context) {
if (compact) {
return IconButton(
key: const ValueKey('dash_quick_feed_button'),
icon: const Icon(
Icons.dynamic_feed_rounded,
color: AppColors.electricCyan,
size: 19,
),
tooltip: 'Squad & Canyon Feed',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
onPressed: () => context.push('/social/feed'),
);
}
return InkWell(
key: const ValueKey('dash_quick_feed_button'),
borderRadius: BorderRadius.circular(16),
onTap: () => context.push('/social/feed'),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF181C21),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.electricCyan.withValues(alpha: 0.3),
width: 0.8,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 7,
height: 7,
decoration: const BoxDecoration(
color: AppColors.electricCyan,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: AppColors.electricCyan,
blurRadius: 4,
),
],
),
),
const SizedBox(width: 5),
const Text(
'FEED',
style: TextStyle(
fontFamily: AppTypography.fontTelemetry,
fontSize: 10,
fontWeight: FontWeight.w800,
letterSpacing: 0.6,
color: Color(0xFFC3F5FF),
),
),
],
),
),
);
}
}

View File

@ -87,9 +87,16 @@ class _ShareSessionModalState extends ConsumerState<ShareSessionModal> {
authorDisplayName: authorDisplayName,
authorHandle: authorHandle,
authorBadge: 'PRO',
locationName: _locationController.text.trim(),
rideTitle: _titleController.text.trim(),
bikeModel: _bikeController.text.trim(),
clubName: 'Bay Area Twisties Club',
locationName: _locationController.text.trim().isNotEmpty
? _locationController.text.trim()
: 'Skyline Blvd & Highway 9',
rideTitle: _titleController.text.trim().isNotEmpty
? _titleController.text.trim()
: 'Morning Canyon Carve',
bikeModel: _bikeController.text.trim().isNotEmpty
? _bikeController.text.trim()
: '2024 Ducati Streetfighter V4',
distanceMiles: dist,
durationSeconds: durMins * 60,
maxLeanAngleDeg: lean,
@ -106,6 +113,9 @@ class _ShareSessionModalState extends ConsumerState<ShareSessionModal> {
],
);
// Reset filter to All Activity so published session is immediately displayed
ref.read(feedFilterStateProvider.notifier).state = FeedFilter.allActivity;
if (mounted) {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(

View File

@ -8,15 +8,42 @@ import '../../services/auth/auth_providers.dart';
import '../../services/social/social_providers.dart';
import 'widgets/avatar_badge.dart';
import 'feed/social_feed_screen.dart';
enum SocialHubTab { hub, feed }
/// The authoritative landing screen for MotorSignal Social & Community.
///
/// Unifies Rider Profile, Friends, and Group Rides under a dedicated,
/// restrained automotive social home completely decoupled from Settings.
class SocialHubScreen extends ConsumerWidget {
const SocialHubScreen({super.key});
/// Unifies the Community Social Feed & Kudos with Rider Profile, Friends,
/// and Group Rides under a dedicated, restrained automotive social home.
class SocialHubScreen extends ConsumerStatefulWidget {
final SocialHubTab initialTab;
const SocialHubScreen({super.key, this.initialTab = SocialHubTab.hub});
@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<SocialHubScreen> createState() => _SocialHubScreenState();
}
class _SocialHubScreenState extends ConsumerState<SocialHubScreen> {
late SocialHubTab _currentTab;
@override
void initState() {
super.initState();
_currentTab = widget.initialTab;
}
@override
void didUpdateWidget(covariant SocialHubScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialTab != widget.initialTab) {
_currentTab = widget.initialTab;
}
}
@override
Widget build(BuildContext context) {
final colors = context.motorColors;
final user = ref.watch(currentUserProvider);
final profileAsync = ref.watch(myRiderProfileProvider);
@ -29,162 +56,246 @@ class SocialHubScreen extends ConsumerWidget {
'Social',
style: TextStyle(fontWeight: FontWeight.w700),
),
),
body: SafeArea(
child: ResponsiveLayout(
builder: (context, responsive) {
final isWide = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 800),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Rider Identity Header Card
_buildRiderHeroCard(context, profileAsync.value, user, colors),
const SizedBox(height: 20),
// Section 1: Rider Network
Text(
'Rider Network',
style: TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 13,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
color: colors.cyberCyan,
),
),
const SizedBox(height: 10),
if (isWide) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _buildNavigationCard(
context,
key: const ValueKey('social_card_friends'),
icon: Icons.people_outline,
title: 'Friends',
subtitle: 'Find fellow riders, manage requests & connections',
badgeCount: pendingCount,
colors: colors,
onTap: () => context.push('/social/friends'),
),
),
const SizedBox(width: 14),
Expanded(
child: _buildNavigationCard(
context,
key: const ValueKey('social_card_clubs'),
icon: Icons.shield_outlined,
title: 'Clubs',
subtitle: 'Discover motorcycle clubs, riding communities & member groups',
colors: colors,
onTap: () => context.push('/social/clubs'),
),
),
],
),
] else ...[
_buildNavigationCard(
context,
key: const ValueKey('social_card_friends'),
icon: Icons.people_outline,
title: 'Friends',
subtitle: 'Find fellow riders, manage requests & connections',
badgeCount: pendingCount,
colors: colors,
onTap: () => context.push('/social/friends'),
),
const SizedBox(height: 12),
_buildNavigationCard(
context,
key: const ValueKey('social_card_clubs'),
icon: Icons.shield_outlined,
title: 'Clubs',
subtitle: 'Discover motorcycle clubs, riding communities & member groups',
colors: colors,
onTap: () => context.push('/social/clubs'),
),
],
const SizedBox(height: 20),
// Section 2: Motorcycle Activities
Text(
'Motorcycle Activities',
style: TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 13,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
color: colors.cyberCyan,
),
),
const SizedBox(height: 10),
if (isWide) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _buildNavigationCard(
context,
key: const ValueKey('social_card_feed'),
icon: Icons.dynamic_feed_outlined,
title: 'Community Feed',
subtitle: 'Squad telemetry, route snapshots, canyon paces & fist-bump kudos',
colors: colors,
onTap: () => context.push('/social/feed'),
),
),
const SizedBox(width: 14),
Expanded(
child: _buildNavigationCard(
context,
key: const ValueKey('social_card_group_rides'),
icon: Icons.groups_outlined,
title: 'Group Rides',
subtitle: 'Join active sessions, explore local pack rides & track group telemetry',
colors: colors,
onTap: () => context.push('/group-rides'),
),
),
],
),
] else ...[
_buildNavigationCard(
context,
key: const ValueKey('social_card_feed'),
icon: Icons.dynamic_feed_outlined,
title: 'Community Feed',
subtitle: 'Squad telemetry, route snapshots, canyon paces & fist-bump kudos',
colors: colors,
onTap: () => context.push('/social/feed'),
),
const SizedBox(height: 12),
_buildNavigationCard(
context,
key: const ValueKey('social_card_group_rides'),
icon: Icons.groups_outlined,
title: 'Group Rides',
subtitle: 'Join active sessions, explore local pack rides & track group telemetry',
colors: colors,
onTap: () => context.push('/group-rides'),
),
],
const SizedBox(height: 24),
],
actions: [
Padding(
padding: const EdgeInsets.only(right: 12),
child: Center(
child: Container(
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: const Color(0xFF181C21),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: colors.outlineVariant.withValues(alpha: 0.5),
width: 0.8,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildTabToggle(
key: const ValueKey('social_tab_feed'),
label: 'Feed',
isSelected: _currentTab == SocialHubTab.feed,
colors: colors,
onTap: () => setState(() => _currentTab = SocialHubTab.feed),
),
_buildTabToggle(
key: const ValueKey('social_tab_hub'),
label: 'Hub',
isSelected: _currentTab == SocialHubTab.hub,
colors: colors,
onTap: () => setState(() => _currentTab = SocialHubTab.hub),
),
],
),
),
);
},
),
),
],
),
body: IndexedStack(
index: _currentTab == SocialHubTab.hub ? 0 : 1,
children: [
_buildHubBody(context, colors, user, profileAsync.value, pendingCount),
const SocialFeedScreen(showAppBar: false),
],
),
);
}
Widget _buildTabToggle({
required Key key,
required String label,
required bool isSelected,
required MotorSignalThemeColors colors,
required VoidCallback onTap,
}) {
return InkWell(
key: key,
borderRadius: BorderRadius.circular(8),
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: isSelected ? colors.cyberCyan : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Text(
label,
style: TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 12,
fontWeight: FontWeight.w700,
color: isSelected ? const Color(0xFF00363D) : const Color(0xFFBAC9CC),
),
),
),
);
}
Widget _buildHubBody(
BuildContext context,
MotorSignalThemeColors colors,
dynamic user,
dynamic profile,
int pendingCount,
) {
return SafeArea(
child: ResponsiveLayout(
builder: (context, responsive) {
final isWide = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 800),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Rider Identity Header Card
_buildRiderHeroCard(context, profile, user, colors),
const SizedBox(height: 20),
// Section 1: Rider Network
Text(
'Rider Network',
style: TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 13,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
color: colors.cyberCyan,
),
),
const SizedBox(height: 10),
if (isWide) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _buildNavigationCard(
context,
key: const ValueKey('social_card_friends'),
icon: Icons.people_outline,
title: 'Friends',
subtitle: 'Find fellow riders, manage requests & connections',
badgeCount: pendingCount,
colors: colors,
onTap: () => context.push('/social/friends'),
),
),
const SizedBox(width: 14),
Expanded(
child: _buildNavigationCard(
context,
key: const ValueKey('social_card_clubs'),
icon: Icons.shield_outlined,
title: 'Clubs',
subtitle: 'Discover motorcycle clubs, riding communities & member groups',
colors: colors,
onTap: () => context.push('/social/clubs'),
),
),
],
),
] else ...[
_buildNavigationCard(
context,
key: const ValueKey('social_card_friends'),
icon: Icons.people_outline,
title: 'Friends',
subtitle: 'Find fellow riders, manage requests & connections',
badgeCount: pendingCount,
colors: colors,
onTap: () => context.push('/social/friends'),
),
const SizedBox(height: 12),
_buildNavigationCard(
context,
key: const ValueKey('social_card_clubs'),
icon: Icons.shield_outlined,
title: 'Clubs',
subtitle: 'Discover motorcycle clubs, riding communities & member groups',
colors: colors,
onTap: () => context.push('/social/clubs'),
),
],
const SizedBox(height: 20),
// Section 2: Motorcycle Activities
Text(
'Motorcycle Activities',
style: TextStyle(
fontFamily: AppTypography.fontHeadline,
fontSize: 13,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
color: colors.cyberCyan,
),
),
const SizedBox(height: 10),
if (isWide) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _buildNavigationCard(
context,
key: const ValueKey('social_card_feed'),
icon: Icons.dynamic_feed_outlined,
title: 'Community Feed',
subtitle: 'Squad telemetry, route snapshots, canyon paces & fist-bump kudos',
colors: colors,
onTap: () => setState(() => _currentTab = SocialHubTab.feed),
),
),
const SizedBox(width: 14),
Expanded(
child: _buildNavigationCard(
context,
key: const ValueKey('social_card_group_rides'),
icon: Icons.groups_outlined,
title: 'Group Rides',
subtitle: 'Join active sessions, explore local pack rides & track group telemetry',
colors: colors,
onTap: () => context.push('/group-rides'),
),
),
],
),
] else ...[
_buildNavigationCard(
context,
key: const ValueKey('social_card_feed'),
icon: Icons.dynamic_feed_outlined,
title: 'Community Feed',
subtitle: 'Squad telemetry, route snapshots, canyon paces & fist-bump kudos',
colors: colors,
onTap: () => setState(() => _currentTab = SocialHubTab.feed),
),
const SizedBox(height: 12),
_buildNavigationCard(
context,
key: const ValueKey('social_card_group_rides'),
icon: Icons.groups_outlined,
title: 'Group Rides',
subtitle: 'Join active sessions, explore local pack rides & track group telemetry',
colors: colors,
onTap: () => context.push('/group-rides'),
),
],
const SizedBox(height: 24),
],
),
),
),
);
},
),
);
}

View File

@ -1,4 +1,6 @@
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'social_feed_models.dart';
import 'social_feed_service.dart';
@ -7,7 +9,9 @@ import 'social_feed_service.dart';
final socialFeedServiceProvider = Provider<SocialFeedService>((ref) {
FirebaseFirestore? firestore;
try {
firestore = FirebaseFirestore.instance;
if (Firebase.apps.isNotEmpty) {
firestore = FirebaseFirestore.instance;
}
} catch (_) {
firestore = null;
}
@ -23,10 +27,40 @@ final feedFilterStateProvider = StateProvider<FeedFilter>((ref) {
return FeedFilter.allActivity;
});
/// Stream provider for the filtered community social feed.
/// StateNotifier providing instant synchronous initial feed items and reacting to changes.
class SocialFeedNotifier extends StateNotifier<List<SocialFeedItem>> {
final SocialFeedService _service;
final FeedFilter _filter;
StreamSubscription<List<SocialFeedItem>>? _sub;
SocialFeedNotifier(this._service, this._filter)
: super(_service.getFilteredItems(filter: _filter)) {
_sub = _service.getFeedStream(filter: _filter).listen((items) {
state = items;
});
}
@override
void dispose() {
_sub?.cancel();
super.dispose();
}
}
/// Primary feed provider with zero-latency synchronous initial data.
final socialFeedNotifierProvider =
StateNotifierProvider.autoDispose<SocialFeedNotifier, List<SocialFeedItem>>(
(ref) {
final service = ref.watch(socialFeedServiceProvider);
final filter = ref.watch(feedFilterStateProvider);
return SocialFeedNotifier(service, filter);
});
/// Stream provider for the filtered community social feed (preserved for backwards compatibility).
final socialFeedStreamProvider =
StreamProvider.autoDispose<List<SocialFeedItem>>((ref) {
final service = ref.watch(socialFeedServiceProvider);
final filter = ref.watch(feedFilterStateProvider);
return service.getFeedStream(filter: filter);
});

View File

@ -124,6 +124,15 @@ class SocialFeedService {
_localFeedController.add(List.unmodifiable(_memoryFeed));
}
List<SocialFeedItem> get currentFeed => List.unmodifiable(_memoryFeed);
List<SocialFeedItem> getFilteredItems({
FeedFilter filter = FeedFilter.allActivity,
String? userClubName,
}) {
return _filterItems(_memoryFeed, filter, userClubName);
}
Stream<List<SocialFeedItem>> getFeedStream({
FeedFilter filter = FeedFilter.allActivity,
String? userClubName,

View File

@ -1,4 +1,4 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:motorcycle_dash/core/router.dart';
void main() {
@ -30,6 +30,12 @@ void main() {
final matchFriends = config.findMatch(Uri.parse('/social/friends'));
expect(matchFriends.matches.isNotEmpty, isTrue);
final matchSocialFeed = config.findMatch(Uri.parse('/social/feed'));
expect(matchSocialFeed.matches.isNotEmpty, isTrue);
final matchFeed = config.findMatch(Uri.parse('/feed'));
expect(matchFeed.matches.isNotEmpty, isTrue);
});
});
}

View File

@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:motorcycle_dash/core/router.dart';
import 'package:motorcycle_dash/core/theme/theme_extensions.dart';
import 'package:motorcycle_dash/features/records/records_service.dart';
import 'package:motorcycle_dash/features/social/feed/social_feed_screen.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
SharedPreferences.setMockInitialValues({});
});
testWidgets('Navigating to /social and tapping Community Feed opens SocialFeedScreen', (tester) async {
await tester.binding.setSurfaceSize(const Size(400, 800));
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
],
child: MaterialApp.router(
theme: ThemeData.dark().copyWith(
extensions: const [
MotorSignalThemeColors.dark,
],
),
routerConfig: appRouter,
),
),
);
await tester.pumpAndSettle();
// 1. We start on Dashboard (index 0)
expect(find.text('Dash'), findsWidgets);
// 2. Tap Social in bottom navigation bar
final socialTab = find.byKey(const ValueKey('nav_social_icon'));
expect(socialTab, findsOneWidget);
await tester.tap(socialTab);
await tester.pumpAndSettle();
// 3. We land directly on Community Feed!
expect(find.byType(SocialFeedScreen), findsOneWidget);
expect(find.text('SQUAD & CANYON FEED'), findsOneWidget);
// 4. Tap 'Hub' toggle in AppBar to view Rider Hub
final hubToggle = find.byKey(const ValueKey('social_tab_hub'));
expect(hubToggle, findsOneWidget);
await tester.tap(hubToggle);
await tester.pumpAndSettle();
expect(find.text('Rider Network'), findsOneWidget);
expect(find.byKey(const ValueKey('social_card_feed')), findsOneWidget);
// 5. Tapping 'Feed' toggle switches back to Community Feed
final feedToggle = find.byKey(const ValueKey('social_tab_feed'));
await tester.tap(feedToggle);
await tester.pumpAndSettle();
expect(find.text('SQUAD & CANYON FEED'), findsOneWidget);
});
}