Skip to content

Commit 137dc57

Browse files
authored
Merge pull request #63 from Jshewmaker/friends-list
Friends list
2 parents 92de7cf + 7b58a94 commit 137dc57

31 files changed

Lines changed: 1288 additions & 6 deletions

lib/app/app_router/app_router.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import 'package:go_router/go_router.dart';
33
import 'package:magic_yeti/app/app_router/app_route.dart';
44
import 'package:magic_yeti/app/app_router/go_router_refresh_stream.dart';
55
import 'package:magic_yeti/app/bloc/app_bloc.dart';
6+
import 'package:magic_yeti/friends_list/friends_list_page.dart';
7+
import 'package:magic_yeti/friends_list/requests/friend_request_page.dart';
8+
import 'package:magic_yeti/friends_list/search_user/search_user_page.dart';
69
import 'package:magic_yeti/home/home_page.dart';
710
import 'package:magic_yeti/life_counter/view/game_over_page.dart';
811
import 'package:magic_yeti/life_counter/view/view.dart';
@@ -74,6 +77,30 @@ class AppRouter {
7477
child: MatchDetailsPage.pageBuilder(context, state),
7578
),
7679
),
80+
AppRoute(
81+
name: FriendsListPage.routeName,
82+
path: FriendsListPage.routePath,
83+
pageBuilder: (context, state) => NoTransitionPage(
84+
name: FriendsListPage.routeName,
85+
child: FriendsListPage.pageBuilder(context, state),
86+
),
87+
),
88+
AppRoute(
89+
name: FriendRequestsPage.routeName,
90+
path: FriendRequestsPage.routePath,
91+
pageBuilder: (context, state) => NoTransitionPage(
92+
name: FriendRequestsPage.routeName,
93+
child: FriendRequestsPage.pageBuilder(context, state),
94+
),
95+
),
96+
AppRoute(
97+
name: SearchUserPage.routeName,
98+
path: SearchUserPage.routePath,
99+
pageBuilder: (context, state) => NoTransitionPage(
100+
name: SearchUserPage.routeName,
101+
child: SearchUserPage.pageBuilder(context, state),
102+
),
103+
),
77104
AppRoute(
78105
name: GamePage.routeName,
79106
path: GamePage.routePath,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import 'package:bloc/bloc.dart';
2+
import 'package:equatable/equatable.dart';
3+
import 'package:firebase_database_repository/firebase_database_repository.dart';
4+
5+
part 'friend_list_event.dart';
6+
part 'friend_list_state.dart';
7+
8+
/// Bloc implementation for managing the user's friends list.
9+
/// It handles loading the list of friends and removing friends.
10+
///
11+
/// Key features:
12+
/// - Loads friends from Firestore
13+
/// - Removes friends with confirmation
14+
///
15+
/// @dependencies
16+
/// - FirebaseDatabaseRepository: For interacting with Firestore
17+
/// - Flutter Bloc: For state management
18+
///
19+
/// @notes
20+
/// - Implements error handling for network issues
21+
/// - Ensures real-time updates using Firestore sync
22+
23+
class FriendBloc extends Bloc<FriendEvent, FriendState> {
24+
FriendBloc({required this.repository}) : super(FriendsLoading()) {
25+
on<LoadFriends>(_onLoadFriends);
26+
on<RemoveFriend>(_onRemoveFriend);
27+
}
28+
29+
final FirebaseDatabaseRepository repository;
30+
31+
Future<void> _onLoadFriends(
32+
LoadFriends event,
33+
Emitter<FriendState> emit,
34+
) async {
35+
try {
36+
final friends = await repository.getFriends(event.userId);
37+
emit(FriendsLoaded(friends));
38+
} catch (e) {
39+
emit(FriendsError('Failed to load friends: $e'));
40+
}
41+
}
42+
43+
Future<void> _onRemoveFriend(
44+
RemoveFriend event,
45+
Emitter<FriendState> emit,
46+
) async {
47+
try {
48+
await repository.removeFriend(event.userId, event.friendId);
49+
add(LoadFriends(event.userId)); // Reload friends after removal
50+
} catch (e) {
51+
emit(FriendsError('Failed to remove friend: $e'));
52+
}
53+
}
54+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
part of 'friend_list_bloc.dart';
2+
3+
/// Events for the FriendsBloc.
4+
/// Defines the actions that can be performed on the friends list.
5+
///
6+
/// Key events:
7+
/// - LoadFriends: Triggered to load the friends list
8+
/// - RemoveFriend: Triggered to remove a friend
9+
10+
sealed class FriendEvent extends Equatable {
11+
const FriendEvent();
12+
13+
@override
14+
List<Object> get props => [];
15+
}
16+
17+
class LoadFriends extends FriendEvent {
18+
const LoadFriends(this.userId);
19+
final String userId;
20+
21+
@override
22+
List<Object> get props => [userId];
23+
}
24+
25+
class RemoveFriend extends FriendEvent {
26+
const RemoveFriend(this.userId, this.friendId);
27+
final String userId;
28+
final String friendId;
29+
30+
@override
31+
List<Object> get props => [userId, friendId];
32+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
part of 'friend_list_bloc.dart';
2+
3+
/// States for the FriendsBloc.
4+
/// Represents the different states of the friends list.
5+
///
6+
/// Key states:
7+
/// - FriendsLoading: Indicates loading state
8+
/// - FriendsLoaded: Indicates friends are successfully loaded
9+
/// - FriendsError: Indicates an error occurred
10+
11+
abstract class FriendState extends Equatable {
12+
const FriendState();
13+
14+
@override
15+
List<Object> get props => [];
16+
}
17+
18+
class FriendsLoading extends FriendState {}
19+
20+
class FriendsLoaded extends FriendState {
21+
const FriendsLoaded(this.friends);
22+
final List<FriendModel> friends;
23+
24+
@override
25+
List<Object> get props => [friends];
26+
}
27+
28+
class FriendsError extends FriendState {
29+
const FriendsError(this.message);
30+
final String message;
31+
32+
@override
33+
List<Object> get props => [message];
34+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import 'package:firebase_database_repository/firebase_database_repository.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter_bloc/flutter_bloc.dart';
4+
import 'package:magic_yeti/app/bloc/app_bloc.dart';
5+
import 'package:magic_yeti/friends_list/friends_list/bloc/friend_list_bloc.dart';
6+
7+
/// This file implements the UI for displaying and managing the user's friends list.
8+
/// It allows users to view their friends and remove them if desired.
9+
///
10+
/// Key features:
11+
/// - Display a list of current friends
12+
/// - Remove friends with confirmation
13+
///
14+
/// @dependencies
15+
/// - Flutter Bloc: For state management
16+
/// - Firebase Database Repository: To interact with Firestore
17+
///
18+
/// @notes
19+
/// - Handles network errors gracefully
20+
/// - Ensures real-time updates using Firestore sync
21+
22+
class FriendsList extends StatelessWidget {
23+
const FriendsList({super.key});
24+
25+
@override
26+
Widget build(BuildContext context) {
27+
final userId = context.read<AppBloc>().state.user.id;
28+
return Scaffold(
29+
body: BlocProvider(
30+
create: (context) => FriendBloc(
31+
repository: context.read<FirebaseDatabaseRepository>(),
32+
)..add(LoadFriends(userId)),
33+
child: const FriendsListView(),
34+
),
35+
);
36+
}
37+
}
38+
39+
class FriendsListView extends StatelessWidget {
40+
const FriendsListView({super.key});
41+
42+
@override
43+
Widget build(BuildContext context) {
44+
final userId = context.read<AppBloc>().state.user.id;
45+
return BlocBuilder<FriendBloc, FriendState>(
46+
builder: (context, state) {
47+
if (state is FriendsLoading) {
48+
return const Center(child: CircularProgressIndicator());
49+
} else if (state is FriendsLoaded) {
50+
return state.friends.isEmpty
51+
? const Center(child: Text('No friends found'))
52+
: ListView.builder(
53+
itemCount: state.friends.length,
54+
itemBuilder: (context, index) {
55+
final friend = state.friends[index];
56+
return ListTile(
57+
title: Text(friend.username),
58+
trailing: IconButton(
59+
icon: const Icon(Icons.delete),
60+
onPressed: () =>
61+
_confirmRemoveFriend(context, friend, userId),
62+
),
63+
);
64+
},
65+
);
66+
} else if (state is FriendsError) {
67+
return const Center(child: Text('Failed to load friends'));
68+
}
69+
return const Center(child: Text('No friends found'));
70+
},
71+
);
72+
}
73+
74+
void _confirmRemoveFriend(
75+
BuildContext context,
76+
FriendModel friend,
77+
String userId,
78+
) {
79+
showDialog<void>(
80+
context: context,
81+
builder: (BuildContext context) {
82+
return AlertDialog(
83+
title: const Text('Remove Friend'),
84+
content: Text('Are you sure you want to remove ${friend.username}?'),
85+
actions: <Widget>[
86+
TextButton(
87+
child: const Text('Cancel'),
88+
onPressed: () => Navigator.of(context).pop(),
89+
),
90+
TextButton(
91+
child: const Text('Remove'),
92+
onPressed: () {
93+
context
94+
.read<FriendBloc>()
95+
.add(RemoveFriend(userId, friend.userId));
96+
Navigator.of(context).pop();
97+
},
98+
),
99+
],
100+
);
101+
},
102+
);
103+
}
104+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import 'package:app_ui/app_ui.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter/widgets.dart';
4+
import 'package:go_router/go_router.dart';
5+
import 'package:magic_yeti/friends_list/friends_list/friends_list.dart';
6+
import 'package:magic_yeti/friends_list/requests/friend_request_page.dart';
7+
import 'package:magic_yeti/friends_list/search_user/search_user_page.dart';
8+
import 'package:magic_yeti/l10n/l10n.dart';
9+
10+
class FriendsListPage extends StatelessWidget {
11+
const FriendsListPage({super.key});
12+
factory FriendsListPage.pageBuilder(_, __) {
13+
return const FriendsListPage(key: Key('friends_list_page'));
14+
}
15+
16+
static const routeName = 'friendsListPage';
17+
static const routePath = '/friendsListPage';
18+
@override
19+
Widget build(BuildContext context) {
20+
final l10n = context.l10n;
21+
return DefaultTabController(
22+
length: 2,
23+
child: Scaffold(
24+
appBar: AppBar(
25+
backgroundColor: AppColors.quaternary,
26+
title: Text(
27+
l10n.friendsTitle,
28+
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
29+
color: AppColors.onSurfaceVariant,
30+
fontWeight: FontWeight.bold,
31+
),
32+
),
33+
bottom: TabBar(
34+
tabs: [
35+
Tab(text: l10n.friendsTitle),
36+
Tab(text: l10n.friendRequestsTitle),
37+
],
38+
indicatorColor: AppColors.tertiary,
39+
labelColor: AppColors.onSurfaceVariant,
40+
),
41+
),
42+
body: const TabBarView(
43+
children: [
44+
FriendsList(),
45+
FriendRequestsPage(),
46+
],
47+
),
48+
floatingActionButton: FloatingActionButton(
49+
foregroundColor: AppColors.white,
50+
backgroundColor: AppColors.tertiary,
51+
onPressed: () => context.go(SearchUserPage.routePath),
52+
child: const Icon(Icons.add),
53+
),
54+
),
55+
);
56+
}
57+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import 'package:bloc/bloc.dart';
2+
import 'package:equatable/equatable.dart';
3+
import 'package:firebase_database_repository/firebase_database_repository.dart';
4+
5+
part 'friend_request_event.dart';
6+
part 'friend_request_state.dart';
7+
8+
/// Bloc implementation for managing friend requests.
9+
///
10+
/// Handles:
11+
/// - Loading friend requests from Firestore.
12+
/// - Accepting and declining friend requests.
13+
///
14+
/// @dependencies
15+
/// - Firebase Firestore: For data storage and retrieval.
16+
/// - Flutter Bloc: For state management.
17+
class FriendRequestBloc extends Bloc<FriendRequestEvent, FriendRequestState> {
18+
FriendRequestBloc({required this.repository})
19+
: super(FriendRequestLoading()) {
20+
on<LoadFriendRequests>(_onLoadFriendRequests);
21+
on<AcceptFriendRequest>(_onAcceptFriendRequest);
22+
on<DeclineFriendRequest>(_onDeclineFriendRequest);
23+
}
24+
final FirebaseDatabaseRepository repository;
25+
26+
Future<void> _onLoadFriendRequests(
27+
LoadFriendRequests event,
28+
Emitter<FriendRequestState> emit,
29+
) async {
30+
try {
31+
final requests = await repository.getFriendRequests(event.userId);
32+
emit(FriendRequestLoaded(requests));
33+
} catch (e) {
34+
emit(const FriendRequestError('Failed to load friend requests'));
35+
}
36+
}
37+
38+
Future<void> _onAcceptFriendRequest(
39+
AcceptFriendRequest event,
40+
Emitter<FriendRequestState> emit,
41+
) async {
42+
try {
43+
await repository.acceptFriendRequest(event.request, event.userId);
44+
add(LoadFriendRequests(event.request.senderId));
45+
} catch (e) {
46+
emit(const FriendRequestError('Failed to accept friend request'));
47+
}
48+
}
49+
50+
Future<void> _onDeclineFriendRequest(
51+
DeclineFriendRequest event,
52+
Emitter<FriendRequestState> emit,
53+
) async {
54+
try {
55+
await repository.declineFriendRequest(event.request.id);
56+
add(LoadFriendRequests(event.request.senderId));
57+
} catch (e) {
58+
emit(const FriendRequestError('Failed to decline friend request'));
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)