diff --git a/lib/forms/matched_screen.dart b/lib/forms/matched_screen.dart index b0b6bff..8f0cb4e 100644 --- a/lib/forms/matched_screen.dart +++ b/lib/forms/matched_screen.dart @@ -8,7 +8,8 @@ class MatchedScreen extends StatelessWidget { final VoidCallback onMessageButtonPressed; final VoidCallback onContinueButtonPressed; - const MatchedScreen({super.key, + const MatchedScreen({ + super.key, required this.user1Name, required this.user2Name, required this.user1ImageUrl, @@ -35,13 +36,20 @@ class MatchedScreen extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ + // TODO imageUrl cant be null or empty with NetworkImage CircleAvatar( - backgroundImage: NetworkImage(user1ImageUrl), + backgroundColor: Colors.blueGrey[300], + backgroundImage: (user1ImageUrl.isEmpty) + ? null + : NetworkImage(user1ImageUrl), radius: 50, ), const SizedBox(width: 24), CircleAvatar( - backgroundImage: NetworkImage(user2ImageUrl), + backgroundColor: Colors.blueGrey[300], + backgroundImage: (user2ImageUrl.isEmpty) + ? null + : NetworkImage(user2ImageUrl), radius: 50, ), ], diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index 8c476b1..1c00776 100644 --- a/lib/pages/chat_page.dart +++ b/lib/pages/chat_page.dart @@ -1,10 +1,11 @@ import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:cofounderella/components/chat_bubble.dart'; -import 'package:cofounderella/components/my_textfield.dart'; -import 'package:cofounderella/services/auth/auth_service.dart'; -import 'package:cofounderella/services/chat/chat_service.dart'; import 'package:flutter/material.dart'; +import '../components/chat_bubble.dart'; +import '../components/my_textfield.dart'; +import '../services/auth/auth_service.dart'; +import '../services/chat/chat_service.dart'; + class ChatPage extends StatefulWidget { final String receiverEmail; final String receiverID; @@ -152,7 +153,7 @@ class _ChatPageState extends State { ), ), ChatBubble( - message: data["message"], + message: data['message'], isCurrentUser: isCurrentUser, ), ], @@ -169,8 +170,8 @@ class _ChatPageState extends State { Expanded( child: MyTextField( controller: _messageController, - hintText: "Type a message", - obscureText: false, // TODO make this optional + hintText: 'Type a message', + obscureText: false, focusNode: myFocusNode, ), ), diff --git a/lib/pages/user_profile_page.dart b/lib/pages/user_profile_page.dart index 2a8a21c..d69e987 100644 --- a/lib/pages/user_profile_page.dart +++ b/lib/pages/user_profile_page.dart @@ -19,7 +19,15 @@ class UserProfilePage extends StatefulWidget { } class UserProfilePageState extends State { + /// List with [all] user profiles List userProfiles = []; + + /// The current's user profile + UserProfile? currentUserProfile; + + /// Other users that yet did either not receive a swipe status + List potentialUserProfiles = []; // potential users for matching + late final SwipableStackController _controller; // get instance of firestore and auth @@ -44,14 +52,32 @@ class UserProfilePageState extends State { } Future _fetchUserProfiles() async { - final querySnapshot = await FirebaseFirestore.instance + final usersSnapshot = await FirebaseFirestore.instance .collection(Constants.dbCollectionUsers) .get(); - final users = await Future.wait(querySnapshot.docs.map((doc) async { - final languagesSnapshot = - await doc.reference.collection(Constants.dbCollectionLanguages).get(); - final locationsSnapshot = - await doc.reference.collection(Constants.dbCollectionLocations).get(); + final String currentUserId = _authService.getCurrentUser()!.uid; + + // Fetch the list of profiles the current user has already swiped + final QuerySnapshot swipesSnapshot = await FirebaseFirestore.instance + .collection(Constants.dbCollectionUsers) + .doc(currentUserId) + .collection(Constants.dbCollectionSwipes) + .get(); + final Set likedUserIds = swipesSnapshot.docs + .where((doc) => doc['liked'] == true) + .map((doc) => doc.id) + .toSet(); + + List allUsers = []; + List showProfiles = []; + + for (var userDoc in usersSnapshot.docs) { + final languagesSnapshot = await userDoc.reference + .collection(Constants.dbCollectionLanguages) + .get(); + final locationsSnapshot = await userDoc.reference + .collection(Constants.dbCollectionLocations) + .get(); final languages = languagesSnapshot.docs.map((doc) { final data = doc.data(); @@ -63,13 +89,10 @@ class UserProfilePageState extends State { ); }).toList(); - final mainDoc = locationsSnapshot.docs.firstWhereOrNull( - (doc) => doc.id == Constants.dbDocMainLocation, - ); - final secondaryDoc = locationsSnapshot.docs.firstWhereOrNull( - (doc) => doc.id == Constants.dbDocSecondLocation, - ); - + final mainDoc = locationsSnapshot.docs + .firstWhereOrNull((doc) => doc.id == Constants.dbDocMainLocation); + final secondaryDoc = locationsSnapshot.docs + .firstWhereOrNull((doc) => doc.id == Constants.dbDocSecondLocation); final locations = { Constants.dbDocMainLocation: mainDoc != null ? _createLocationFromDoc(mainDoc.data()) : null, @@ -78,9 +101,9 @@ class UserProfilePageState extends State { : null, }; - final data = doc.data(); - return UserProfile( - id: doc.id, + final data = userDoc.data(); + UserProfile userProfile = UserProfile( + id: userDoc.id, uid: data[Constants.dbFieldUsersID] ?? '', email: data[Constants.dbFieldUsersEmail] ?? '', name: data[Constants.dbFieldUsersName] ?? '', @@ -93,11 +116,21 @@ class UserProfilePageState extends State { languages: languages, locations: locations, ); - }).toList()); - setState(() { - userProfiles = users; - }); + // add profiles accordingly + allUsers.add(userProfile); + // Exclude the current user's profile and the already liked profiles + if (userDoc.id != currentUserId && !likedUserIds.contains(userDoc.id)) { + showProfiles.add(userProfile); + } + + setState(() { + userProfiles = allUsers; + potentialUserProfiles = showProfiles; + currentUserProfile = + allUsers.firstWhereOrNull((x) => x.uid == currentUserId); + }); + } } MyLocation? _createLocationFromDoc(Map? data) { @@ -117,7 +150,7 @@ class UserProfilePageState extends State { void _swipeLeft() { _controller.next( - swipeDirection: SwipeDirection.left, duration: Durations.extralong4); + swipeDirection: SwipeDirection.left, duration: Durations.extralong1); } void _swipeRight() { @@ -127,7 +160,7 @@ class UserProfilePageState extends State { void _skip() { _controller.next( - swipeDirection: SwipeDirection.up, duration: Durations.extralong2); + swipeDirection: SwipeDirection.up, duration: Durations.long4); } /// Save swipe status to database @@ -151,7 +184,7 @@ class UserProfilePageState extends State { return userProfiles.firstWhere((x) => x.uid == userId); } - /// Check whether the swiped user has also swiped current user to the right + /// Check whether the swiped user has also swiped to the right Future _checkForMatch(swipedUserId) async { String currentUserId = _authService.getCurrentUser()!.uid; @@ -185,11 +218,8 @@ class UserProfilePageState extends State { }); // - // TODO Notification and further logic, e.g. initialization of the chat + // TODO Notify other user? // - print( - 'We have a match between ${getUserProfile(currentUserId).name} and ${getUserProfile(swipedUserId).name}'); - showMatchedScreen(currentUserId, swipedUserId); } } @@ -228,10 +258,32 @@ class UserProfilePageState extends State { @override Widget build(BuildContext context) { - if (userProfiles.isEmpty) { + if (potentialUserProfiles.isEmpty) { return Scaffold( appBar: AppBar(title: const Text('User Profiles')), - body: const Center(child: CircularProgressIndicator()), + body: Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.person_search, size: 64), + const SizedBox(height: 20), + Text( + '${userProfiles.length > 1 ? 'No new' : 'No'} profiles available at the moment.', + style: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 18), + ), + const SizedBox(height: 60), + const Text( + 'Please check back later, perhaps tomorrow.', + textAlign: TextAlign.center, + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + ), + ], + ), + ), + ), ); } @@ -253,13 +305,14 @@ class UserProfilePageState extends State { controller: _controller, stackClipBehaviour: Clip.none, swipeAnchor: SwipeAnchor.bottom, - itemCount: userProfiles.length+1, // +1 for rerun option + // item count +1 for additional end page + itemCount: potentialUserProfiles.length + 1, onSwipeCompleted: (index, direction) { // - // Swipe logic goes here + // Swipe logic // String swipedUserId = - userProfiles[_controller.currentIndex].id; + potentialUserProfiles[_controller.currentIndex].id; if (direction == SwipeDirection.right) { _saveSwipeAction(swipedUserId, SwipeDirection.right); _checkForMatch(swipedUserId); @@ -269,40 +322,15 @@ class UserProfilePageState extends State { }, horizontalSwipeThreshold: 0.8, verticalSwipeThreshold: 0.8, - /*overlayBuilder: (context, properties) { - final opacity = min(properties.swipeProgress, 1.0); - final isRight = properties.direction == SwipeDirection.right; - return Opacity( - opacity: isRight ? opacity : 0, - child: CardLabel.right(), - ); - },*/ builder: (context, properties) { - - if (properties.index == userProfiles.length) { + if (properties.index == potentialUserProfiles.length) { return Center( - child: Column( // Show end message and restart button - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('That\'s all.\nDo you want to do another run?', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold) - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () { - setState(() { - _controller.currentIndex = 0; // Restart swiping from the beginning - }); - }, - child: const Text('Rerun'), - ), - ], - ), + child: _buildLastCard(), // last card ); } - final userProfile = - userProfiles[properties.index % userProfiles.length]; + final userProfile = potentialUserProfiles[ + properties.index % potentialUserProfiles.length]; return Container( alignment: Alignment.center, decoration: BoxDecoration( @@ -311,34 +339,7 @@ class UserProfilePageState extends State { ), child: Stack( children: [ - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(userProfile.name, - style: const TextStyle(fontSize: 24)), - Text(userProfile.email, - style: const TextStyle(fontSize: 24)), - const SizedBox(height: 8), - Text( - 'Has skills and experience in: ${userProfile.skills.join(', ')}'), - Text( - 'Seeks someone with skills in: ${userProfile.skillsSought.join(', ')}'), - Text('Risk type: ${userProfile.risk}'), - Text( - 'Speaks: ${userProfile.languages.map((lang) => lang.name).join(', ')}'), - Text( - 'Lives in: ${userProfile.locations['main']?.locality ?? 'N/A'}'), - Text( - 'Second home: ${userProfile.locations['secondary']?.locality ?? 'N/A'}'), - ], - ), - ), - ), - ), + _buildUserCard(userProfile), if (properties.stackIndex == 0 && properties.direction != null) CardOverlay( @@ -352,55 +353,152 @@ class UserProfilePageState extends State { ), ), ), - Positioned( - bottom: 16, - left: 0, - right: 0, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - FloatingActionButton( - tooltip: 'Undo last action', - shape: const CircleBorder(), - onPressed: () { - _controller.rewind(duration: Durations.extralong4); - }, - child: const Icon(Icons.undo), - ), - SizedBox( - width: 72, - height: 72, - child: FloatingActionButton( - shape: const CircleBorder(), - onPressed: _swipeLeft, - child: - const Icon(Icons.cancel, color: Colors.red, size: 64), - ), - ), - SizedBox( - width: 72, - height: 72, - child: FloatingActionButton( - shape: const CircleBorder(), - onPressed: _swipeRight, - child: const Icon(Icons.check_circle, - color: Colors.green, size: 64), - ), - ), - FloatingActionButton( - tooltip: 'Skip profile', - shape: const CircleBorder(), - onPressed: _skip, - child: const Icon(Icons.skip_next), - ), - ], - ), + _buildSwipeButtons(), + ], + ), + ), + ); + } + + Widget _buildUserCard(UserProfile userProfile) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(userProfile.name, style: const TextStyle(fontSize: 24)), + Text(userProfile.email, style: const TextStyle(fontSize: 24)), + const SizedBox(height: 8), + Text( + 'Has skills and experience in: ${userProfile.skills.join(', ')}'), + Text( + 'Seeks someone with skills in: ${userProfile.skillsSought.join(', ')}'), + Text('Risk type: ${userProfile.risk}'), + Text( + 'Speaks: ${userProfile.languages.map((lang) => lang.name).join(', ')}'), + Text( + 'Lives in: ${userProfile.locations[Constants.dbDocMainLocation]?.locality ?? 'N/A'}'), + Text( + 'Second home: ${userProfile.locations[Constants.dbDocSecondLocation]?.locality ?? 'N/A'}'), + ], + ), + ), + ), + ); + } + + Widget _buildLastCard() { + return Padding( + padding: const EdgeInsets.all(16.0), + child: SingleChildScrollView( + child: Column( + // Show reached end message and restart button + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.person_search, size: 64), + const SizedBox(height: 20), + const Text('You\'ve viewed all available profiles.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), + const SizedBox(height: 60), + const Text( + 'Would you like to do another run and see the remaining profiles again?', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + setState(() { + // Restart swiping from the beginning + _controller.currentIndex = 0; + }); + }, + child: const Text('Another run'), ), ], ), ), ); } + + Widget _buildSwipeButtons() { + return Positioned( + bottom: 16, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + FloatingActionButton( + heroTag: 'myFABUndo', + tooltip: 'Undo last action', + shape: const CircleBorder(), + onPressed: () { + _controller.rewind(duration: Durations.extralong4); + }, + child: const Icon(Icons.undo), + ), + SizedBox( + width: 72, + height: 72, + child: FloatingActionButton( + heroTag: 'myFABSwipeLeft', + shape: const CircleBorder(), + onPressed: _swipeLeft, + child: Stack( + // to deal with icons inner transparency + alignment: Alignment.center, + children: [ + Container( + width: 48.0, + height: 48.0, + decoration: const BoxDecoration( + color: Colors.white70, + shape: BoxShape.circle, + ), + ), + const Icon(Icons.cancel, color: Colors.red, size: 72), + ], + ), + ), + ), + SizedBox( + width: 72, + height: 72, + child: FloatingActionButton( + heroTag: 'myFABSwipeRight', + shape: const CircleBorder(), + onPressed: _swipeRight, + child: Stack( + // to deal with icons inner transparency + alignment: Alignment.center, + children: [ + Container( + width: 48.0, + height: 48.0, + decoration: const BoxDecoration( + color: Colors.white70, + shape: BoxShape.circle, + ), + ), + const Icon(Icons.check_circle, color: Colors.green, size: 72), + ], + ), + ), + ), + FloatingActionButton( + tooltip: 'Skip profile', + heroTag: 'myFABSkip', + shape: const CircleBorder(), + onPressed: _skip, + child: const Icon(Icons.skip_next), + ), + ], + ), + ); + } } class CardOverlay extends StatelessWidget { @@ -427,9 +525,9 @@ class CardOverlay extends StatelessWidget { : Alignment.center), child: Icon( direction == SwipeDirection.right - ? Icons.check_circle + ? Icons.thumb_up : (direction == SwipeDirection.left - ? Icons.cancel + ? Icons.thumb_down : Icons.skip_next), size: 100, color: direction == SwipeDirection.right