import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/error_mapper.dart'; import '../../core/responsive/responsive_layout.dart'; import '../../core/responsive/viewport_size.dart'; import '../../core/theme/theme_extensions.dart'; import '../../services/auth/auth_providers.dart'; import '../../services/social/rider_profile_models.dart'; import '../../services/social/social_providers.dart'; import 'widgets/avatar_badge.dart'; /// Screen managing friends, requests, and rider discovery. class FriendsScreen extends ConsumerStatefulWidget { const FriendsScreen({super.key}); @override ConsumerState createState() => _FriendsScreenState(); } class _FriendsScreenState extends ConsumerState with SingleTickerProviderStateMixin { late TabController _tabController; final _searchController = TextEditingController(); bool _isSearching = false; RiderSearchResult? _searchResult; String? _searchError; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); } @override void dispose() { _tabController.dispose(); _searchController.dispose(); super.dispose(); } Future _performSearch() async { final query = _searchController.text.trim(); if (query.isEmpty) return; final user = ref.read(currentUserProvider); if (user == null || user.isAnonymous) return; setState(() { _isSearching = true; _searchError = null; _searchResult = null; }); try { final searchService = ref.read(riderSearchServiceProvider); final result = await searchService.searchByHandle( rawHandle: query, callerUid: user.uid, ); if (mounted) { setState(() { _isSearching = false; _searchResult = result; if (result == null) { _searchError = 'No rider found with handle "$query"'; } }); } } catch (e) { if (mounted) { setState(() { _isSearching = false; _searchError = 'Search error: ${ErrorMapper.map(e)}'; }); } } } Future _sendFriendRequest(RiderSearchResult target) async { final user = ref.read(currentUserProvider); final myProfileAsync = ref.read(myRiderProfileProvider); final myProfile = myProfileAsync.value; if (user == null || user.isAnonymous || myProfile == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Please create your rider profile first.'), ), ); return; } try { final friendshipService = ref.read(friendshipServiceProvider); await friendshipService.sendFriendRequest( sender: myProfile, receiverUid: target.uid, ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Friend request sent to @${target.handle}')), ); } await _performSearch(); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))), ); } } } Future _acceptFriendRequest(FriendRequest request) async { final myProfile = ref.read(myRiderProfileProvider).value; if (myProfile == null) return; try { final friendshipService = ref.read(friendshipServiceProvider); await friendshipService.acceptFriendRequest( me: myProfile, request: request, ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Now friends with @${request.senderHandle}!'), ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error accepting request: ${ErrorMapper.map(e)}')), ); } } } Future _declineFriendRequest(FriendRequest request) async { final user = ref.read(currentUserProvider); if (user == null) return; try { final friendshipService = ref.read(friendshipServiceProvider); await friendshipService.declineFriendRequest( myUid: user.uid, senderUid: request.senderUid, ); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error declining request: ${ErrorMapper.map(e)}')), ); } } } Future _cancelFriendRequest(String receiverUid) async { final user = ref.read(currentUserProvider); if (user == null) return; try { final friendshipService = ref.read(friendshipServiceProvider); await friendshipService.cancelFriendRequest( myUid: user.uid, receiverUid: receiverUid, ); if (_searchResult?.uid == receiverUid) { await _performSearch(); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error canceling request: ${ErrorMapper.map(e)}')), ); } } } Future _removeFriend(FriendSummary friend) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text('Remove @${friend.handle}?'), content: Text( 'Are you sure you want to remove ${friend.displayName} from your friends list?', ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('CANCEL'), ), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent), onPressed: () => Navigator.of(ctx).pop(true), child: const Text('REMOVE'), ), ], ), ); if (confirmed != true) return; final user = ref.read(currentUserProvider); if (user == null) return; try { final friendshipService = ref.read(friendshipServiceProvider); await friendshipService.removeFriend( myUid: user.uid, friendUid: friend.uid, ); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error removing friend: ${ErrorMapper.map(e)}')), ); } } } Future _blockRider(String targetUid, String handle, String name) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text('Block @$handle?'), content: Text( 'Are you sure you want to block $name? They will not be able to find your profile, send requests, or see your activity.', ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('CANCEL'), ), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent), onPressed: () => Navigator.of(ctx).pop(true), child: const Text('BLOCK'), ), ], ), ); if (confirmed != true) return; final user = ref.read(currentUserProvider); if (user == null) return; try { final friendshipService = ref.read(friendshipServiceProvider); await friendshipService.blockRider( myUid: user.uid, targetUid: targetUid, targetHandle: handle, targetDisplayName: name, ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Blocked @$handle')), ); setState(() { _searchResult = null; }); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error blocking rider: ${ErrorMapper.map(e)}')), ); } } } @override Widget build(BuildContext context) { final colors = context.motorColors; final authState = ref.watch(authNotifierProvider); final isAuthed = authState.isAuthenticated; if (!isAuthed) { return Scaffold( backgroundColor: colors.surfaceContainer, appBar: AppBar( backgroundColor: colors.surfaceContainerHigh, title: const Text( 'Friends & Riders', style: TextStyle(fontWeight: FontWeight.w800), ), ), body: Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.group_outlined, size: 64, color: colors.cyberCyan, ), const SizedBox(height: 16), const Text( 'MOTORCLOUD ACCOUNT REQUIRED', style: TextStyle( fontWeight: FontWeight.w800, fontSize: 16, color: Colors.white, letterSpacing: 0.8, ), ), const SizedBox(height: 8), const Text( 'Sign in to add riding buddies, share meetups, and coordinate future group rides.', textAlign: TextAlign.center, style: TextStyle(fontSize: 13, color: Colors.white70), ), const SizedBox(height: 24), ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, ), icon: const Icon(Icons.login), label: const Text('SIGN IN OR REGISTER'), onPressed: () => context.push('/settings/account'), ), ], ), ), ), ); } final friendsAsync = ref.watch(myFriendsProvider); final incomingAsync = ref.watch(myIncomingRequestsProvider); final outgoingAsync = ref.watch(myOutgoingRequestsProvider); final incomingCount = incomingAsync.value?.length ?? 0; final friendsCount = friendsAsync.value?.length ?? 0; return Scaffold( backgroundColor: colors.surfaceContainer, appBar: AppBar( backgroundColor: colors.surfaceContainerHigh, title: const Text( 'Friends & Riders', style: TextStyle(fontWeight: FontWeight.w800), ), actions: [ IconButton( icon: const Icon(Icons.person_outline), tooltip: 'My Profile', onPressed: () => context.push('/settings/profile'), ), ], bottom: TabBar( controller: _tabController, isScrollable: false, tabAlignment: TabAlignment.fill, labelPadding: EdgeInsets.zero, indicatorColor: colors.cyberCyan, labelColor: colors.cyberCyan, unselectedLabelColor: Colors.white60, tabs: [ Tab( child: FittedBox( fit: BoxFit.scaleDown, child: Text('Friends ($friendsCount)'), ), ), Tab( child: FittedBox( fit: BoxFit.scaleDown, child: Row( mainAxisSize: MainAxisSize.min, children: [ const Text('Requests'), if (incomingCount > 0) ...[ const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: colors.warningAmber, borderRadius: BorderRadius.circular(10), ), child: Text( '$incomingCount', style: const TextStyle( color: Colors.black, fontSize: 10, fontWeight: FontWeight.w900, ), ), ), ], ], ), ), ), const Tab( child: FittedBox( fit: BoxFit.scaleDown, child: Text('Find Riders'), ), ), ], ), ), body: TabBarView( controller: _tabController, children: [ // TAB 1: FRIENDS LIST friendsAsync.when( loading: () => Center( child: CircularProgressIndicator(color: colors.cyberCyan), ), error: (err, _) => Center( child: Text( 'Error: ${ErrorMapper.map(err)}', style: TextStyle(color: colors.raceRed), ), ), data: (friends) { if (friends.isEmpty) { return SingleChildScrollView( child: Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.people_alt_outlined, size: 48, color: Colors.white24, ), const SizedBox(height: 12), const Text( 'NO FRIENDS ADDED YET', style: TextStyle( color: Colors.white70, fontWeight: FontWeight.w800, fontSize: 14, letterSpacing: 0.8, ), ), const SizedBox(height: 6), const Text( 'Switch to "Find Riders" to search for riding buddies by @handle.', textAlign: TextAlign.center, style: TextStyle(color: Colors.white38, fontSize: 12), ), const SizedBox(height: 16), ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, ), icon: const Icon(Icons.search), label: const Text('FIND RIDERS'), onPressed: () => _tabController.animateTo(2), ), ], ), ), ), ); } return ResponsiveLayout( builder: (context, snapshot) { final isWide = snapshot.sizeClass == WindowSizeClass.expanded || (snapshot.isLandscape && snapshot.width >= 720); if (isWide) { final crossAxisCount = snapshot.width >= 1100 ? 3 : 2; return GridView.builder( padding: const EdgeInsets.all(20), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, crossAxisSpacing: 16, mainAxisSpacing: 16, mainAxisExtent: 88, ), itemCount: friends.length, itemBuilder: (context, index) { final friend = friends[index]; return Card( color: colors.surfaceContainerHigh, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 8, ), child: Row( children: [ AvatarBadge( avatarPreset: friend.avatarPreset, photoUrl: friend.avatarUrl, displayName: friend.displayName, radius: 24, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( friend.displayName, overflow: TextOverflow.ellipsis, style: const TextStyle( fontWeight: FontWeight.w700, color: Colors.white, ), ), Text( friend.displayHandle, overflow: TextOverflow.ellipsis, style: TextStyle( color: colors.cyberCyan, fontSize: 12, ), ), ], ), ), _buildFriendMenu(friend, colors), ], ), ), ); }, ); } return ListView.separated( padding: const EdgeInsets.all(16.0), itemCount: friends.length, separatorBuilder: (context, index) => Divider(color: colors.outlineVariant), itemBuilder: (context, index) { final friend = friends[index]; return ListTile( contentPadding: EdgeInsets.zero, leading: AvatarBadge( avatarPreset: friend.avatarPreset, photoUrl: friend.avatarUrl, displayName: friend.displayName, radius: 24, ), title: Text( friend.displayName, style: const TextStyle( fontWeight: FontWeight.w700, color: Colors.white, ), ), subtitle: Text( friend.displayHandle, style: TextStyle( color: colors.cyberCyan, fontSize: 12, ), ), trailing: _buildFriendMenu(friend, colors), ); }, ); }, ); }, ), // TAB 2: REQUESTS ListView( padding: const EdgeInsets.all(16.0), children: [ // Incoming Section Text( 'INCOMING REQUESTS', style: TextStyle( color: colors.cyberCyan, fontSize: 12, fontWeight: FontWeight.w800, letterSpacing: 0.8, ), ), const SizedBox(height: 8), incomingAsync.when( loading: () => Center( child: Padding( padding: const EdgeInsets.all(16.0), child: CircularProgressIndicator(color: colors.cyberCyan), ), ), error: (err, _) => Text( 'Error: ${ErrorMapper.map(err)}', style: TextStyle(color: colors.raceRed), ), data: (requests) { if (requests.isEmpty) { return const Padding( padding: EdgeInsets.symmetric(vertical: 12.0), child: Text( 'No pending incoming requests', style: TextStyle(color: Colors.white38, fontSize: 12), ), ); } return Column( children: requests.map((req) { return Card( color: colors.surfaceContainerHigh, margin: const EdgeInsets.only(bottom: 8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(12.0), child: Row( children: [ AvatarBadge( avatarPreset: req.senderAvatarPreset, photoUrl: req.senderAvatarUrl, displayName: req.senderDisplayName, radius: 20, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( req.senderDisplayName, style: const TextStyle( fontWeight: FontWeight.w700, color: Colors.white, ), ), Text( '@${req.senderHandle}', style: TextStyle( color: colors.cyberCyan, fontSize: 11, ), ), ], ), ), IconButton( icon: Icon( Icons.check_circle_outline, color: colors.launchGreen, ), tooltip: 'Accept', onPressed: () => _acceptFriendRequest(req), ), IconButton( icon: Icon( Icons.cancel_outlined, color: colors.raceRed, ), tooltip: 'Decline', onPressed: () => _declineFriendRequest(req), ), ], ), ), ); }).toList(), ); }, ), const SizedBox(height: 24), // Outgoing Section const Text( 'SENT REQUESTS', style: TextStyle( color: Colors.white70, fontSize: 12, fontWeight: FontWeight.w800, letterSpacing: 0.8, ), ), const SizedBox(height: 8), outgoingAsync.when( loading: () => const SizedBox.shrink(), error: (err, _) => Text( 'Error: ${ErrorMapper.map(err)}', style: TextStyle(color: colors.raceRed), ), data: (requests) { if (requests.isEmpty) { return const Padding( padding: EdgeInsets.symmetric(vertical: 12.0), child: Text( 'No pending sent requests', style: TextStyle(color: Colors.white38, fontSize: 12), ), ); } return Column( children: requests.map((req) { return ListTile( contentPadding: EdgeInsets.zero, leading: const Icon( Icons.outgoing_mail, color: Colors.white54, ), title: Text( 'To Rider (${req.receiverUid.substring(0, req.receiverUid.length.clamp(0, 8))}...)', style: const TextStyle( fontSize: 13, color: Colors.white, ), ), subtitle: Text( 'Sent ${req.sentAt.toLocal().toString().split('.')[0]}', style: const TextStyle( fontSize: 11, color: Colors.white38, ), ), trailing: TextButton( onPressed: () => _cancelFriendRequest(req.receiverUid), child: const Text('CANCEL'), ), ); }).toList(), ); }, ), ], ), // TAB 3: FIND RIDERS Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Expanded( child: TextField( controller: _searchController, style: const TextStyle(color: Colors.white), decoration: InputDecoration( hintText: 'Enter @handle (e.g. speed_demon)', prefixIcon: const Icon(Icons.search), border: const OutlineInputBorder(), filled: true, fillColor: colors.surfaceContainerHigh, ), onSubmitted: (_) => _performSearch(), ), ), const SizedBox(width: 8), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 16, ), ), onPressed: _isSearching ? null : _performSearch, child: _isSearching ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.black, ), ) : const Text('SEARCH'), ), ], ), const SizedBox(height: 24), if (_searchError != null) Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Text( _searchError!, style: const TextStyle( color: Colors.white60, fontSize: 13, ), ), ), ), if (_searchResult != null) ...[ Card( color: colors.surfaceContainerHigh, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ AvatarBadge( avatarPreset: _searchResult!.avatarPreset, photoUrl: _searchResult!.avatarUrl, displayName: _searchResult!.displayName, radius: 28, ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _searchResult!.displayName, style: const TextStyle( fontWeight: FontWeight.w800, fontSize: 16, color: Colors.white, ), ), Text( _searchResult!.displayHandle, style: TextStyle( color: colors.cyberCyan, fontWeight: FontWeight.w700, fontSize: 13, ), ), if (_searchResult!.region.isNotEmpty) ...[ const SizedBox(height: 2), Text( _searchResult!.region, style: const TextStyle( color: Colors.white60, fontSize: 11, ), ), ], ], ), ), ], ), if (_searchResult!.bio.isNotEmpty) ...[ const SizedBox(height: 12), Text( _searchResult!.bio, style: const TextStyle( color: Colors.white70, fontSize: 12, fontStyle: FontStyle.italic, ), ), ], Divider(height: 24, color: colors.outlineVariant), // Relationship Action Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (_searchResult!.relationship == FriendshipStatus.self) const Text( 'This is your profile', style: TextStyle( color: Colors.white38, fontSize: 12, ), ) else if (_searchResult!.relationship == FriendshipStatus.friend) Chip( avatar: Icon(Icons.check, size: 16, color: colors.cyberCyan), label: const Text('Friends'), backgroundColor: colors.cyberCyan.withValues(alpha: 0.15), labelStyle: TextStyle( color: colors.cyberCyan, ), ) else if (_searchResult!.relationship == FriendshipStatus.requestOutgoing) ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: colors.surfaceContainerHighest, foregroundColor: Colors.white, ), icon: const Icon(Icons.cancel_outlined, size: 16), label: const Text('CANCEL REQUEST'), onPressed: () => _cancelFriendRequest(_searchResult!.uid), ) else if (_searchResult!.relationship == FriendshipStatus.requestIncoming) Text( 'Check Requests tab to respond', style: TextStyle( color: colors.warningAmber, fontSize: 12, ), ) else if (!_searchResult!.allowFriendRequests) const Text( 'Not accepting friend requests', style: TextStyle( color: Colors.white38, fontSize: 12, ), ) else ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: colors.cyberCyan, foregroundColor: Colors.black, ), icon: const Icon(Icons.person_add, size: 16), label: const Text('ADD FRIEND'), onPressed: () => _sendFriendRequest(_searchResult!), ), if (_searchResult!.relationship != FriendshipStatus.self) IconButton( icon: const Icon( Icons.block, color: Colors.white38, size: 18, ), tooltip: 'Block Rider', onPressed: () => _blockRider( _searchResult!.uid, _searchResult!.handle, _searchResult!.displayName, ), ), ], ), ], ), ), ), ], ], ), ), ], ), ); } Widget _buildFriendMenu( FriendSummary friend, MotorSignalThemeColors colors) { return PopupMenuButton( icon: const Icon(Icons.more_vert, color: Colors.white60), color: colors.surfaceContainerHighest, onSelected: (val) { if (val == 'remove') { _removeFriend(friend); } else if (val == 'block') { _blockRider(friend.uid, friend.handle, friend.displayName); } }, itemBuilder: (context) => [ const PopupMenuItem( value: 'remove', child: Row( children: [ Icon(Icons.person_remove_outlined, size: 18), SizedBox(width: 8), Text('Remove Friend'), ], ), ), PopupMenuItem( value: 'block', child: Row( children: [ Icon(Icons.block, size: 18, color: colors.raceRed), const SizedBox(width: 8), Text('Block Rider', style: TextStyle(color: colors.raceRed)), ], ), ), ], ); } }