diff --git a/lib/constants.dart b/lib/constants.dart index 99d5fca..b17a254 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -8,6 +8,8 @@ class Constants { static const String dbCollectionUsers = 'Users'; static const String dbCollectionLanguages = 'languages'; static const String dbCollectionLocations = 'locations'; + static const String dbCollectionMatches = 'matches'; + static const String dbCollectionSwipes = 'swipes'; static const String dbCollectionChatRooms = 'chat_rooms'; static const String dbCollectionMessages = 'messages'; diff --git a/lib/forms/matched_screen.dart b/lib/forms/matched_screen.dart new file mode 100644 index 0000000..b0b6bff --- /dev/null +++ b/lib/forms/matched_screen.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +class MatchedScreen extends StatelessWidget { + final String user1Name; + final String user2Name; + final String user1ImageUrl; + final String user2ImageUrl; + final VoidCallback onMessageButtonPressed; + final VoidCallback onContinueButtonPressed; + + const MatchedScreen({super.key, + required this.user1Name, + required this.user2Name, + required this.user1ImageUrl, + required this.user2ImageUrl, + required this.onMessageButtonPressed, + required this.onContinueButtonPressed, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('It\'s a Match!')), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'You and $user2Name have liked each other!', + style: const TextStyle(fontSize: 24), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircleAvatar( + backgroundImage: NetworkImage(user1ImageUrl), + radius: 50, + ), + const SizedBox(width: 24), + CircleAvatar( + backgroundImage: NetworkImage(user2ImageUrl), + radius: 50, + ), + ], + ), + const SizedBox(height: 24), + Text( + '$user1Name and $user2Name', + style: const TextStyle(fontSize: 20), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: onMessageButtonPressed, + child: const Text('Send a Message'), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: onContinueButtonPressed, + child: const Text('Continue Swiping'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/helper.dart b/lib/helper.dart index 89df3a4..7172a7f 100644 --- a/lib/helper.dart +++ b/lib/helper.dart @@ -8,6 +8,15 @@ bool equalContent(List list1, List list2) { return const DeepCollectionEquality.unordered().equals(list1, list2); } +/// +/// Creates a composite ID from the passed [ids]. +/// In the format id(1)_id(n) +/// +String getCompoundId(List ids){ + ids.sort(); // sort to ensure the result is the same for any order of ids + return ids.join('_'); +} + /// /// Convert decimal coordinate to degrees minutes seconds (DMS). /// @@ -30,7 +39,7 @@ String convertDecimalToDMS(double decimalValue) { } /// -/// Get the displayName of our own Enumerations. +/// Get the [displayName] of our own Enumerations. /// String getDisplayText(dynamic option) { // Check if the option is an enum and has a displayName property diff --git a/lib/models/user_profile.dart b/lib/models/user_profile.dart index 47dc9e5..8091caf 100644 --- a/lib/models/user_profile.dart +++ b/lib/models/user_profile.dart @@ -36,11 +36,11 @@ class UserProfile { return UserProfile( id: doc.id, + uid: data[Constants.dbFieldUsersID] ?? '', email: data[Constants.dbFieldUsersEmail] ?? '', name: data[Constants.dbFieldUsersName] ?? '', firstName: data[Constants.dbFieldUsersFirstName] ?? '', lastName: data[Constants.dbFieldUsersLastName] ?? '', - uid: data[Constants.dbFieldUsersID] ?? '', skills: List.from(data[Constants.dbFieldUsersSkills] ?? []), skillsSought: List.from(data[Constants.dbFieldUsersSkillsSought] ?? []), diff --git a/lib/pages/user_profile_page.dart b/lib/pages/user_profile_page.dart index 081fb95..2a8a21c 100644 --- a/lib/pages/user_profile_page.dart +++ b/lib/pages/user_profile_page.dart @@ -4,9 +4,12 @@ import 'package:flutter/material.dart'; import 'package:swipable_stack/swipable_stack.dart'; import '../constants.dart'; +import '../forms/matched_screen.dart'; import '../models/language.dart'; import '../models/location.dart'; import '../models/user_profile.dart'; +import '../services/auth/auth_service.dart'; +import 'chat_page.dart'; class UserProfilePage extends StatefulWidget { const UserProfilePage({super.key}); @@ -19,6 +22,10 @@ class UserProfilePageState extends State { List userProfiles = []; late final SwipableStackController _controller; + // get instance of firestore and auth + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final AuthService _authService = AuthService(); + void _listenController() => setState(() {}); @override @@ -109,13 +116,11 @@ class UserProfilePageState extends State { } void _swipeLeft() { - _saveSwipeAction(userProfiles[_controller.currentIndex].id, 'dislike'); _controller.next( swipeDirection: SwipeDirection.left, duration: Durations.extralong4); } void _swipeRight() { - _saveSwipeAction(userProfiles[_controller.currentIndex].id, 'like'); _controller.next( swipeDirection: SwipeDirection.right, duration: Durations.extralong4); } @@ -125,12 +130,100 @@ class UserProfilePageState extends State { swipeDirection: SwipeDirection.up, duration: Durations.extralong2); } - void _saveSwipeAction(String userId, String action) { -/* FirebaseFirestore.instance.collection('swipes').add({ - 'userId': userId, - 'action': action, - 'timestamp': FieldValue.serverTimestamp(), - });*/ + /// Save swipe status to database + Future _saveSwipeAction( + String swipedUserId, SwipeDirection direction) async { + await _firestore + .collection(Constants.dbCollectionUsers) + .doc(_authService.getCurrentUser()!.uid) + .collection(Constants.dbCollectionSwipes) + .doc(swipedUserId) // UserID instead of autogenerated ID + .set( + { + 'swipedId': swipedUserId, + 'liked': direction == SwipeDirection.right, + 'timestamp': FieldValue.serverTimestamp(), + }, + ); + } + + UserProfile getUserProfile(String userId) { + return userProfiles.firstWhere((x) => x.uid == userId); + } + + /// Check whether the swiped user has also swiped current user to the right + Future _checkForMatch(swipedUserId) async { + String currentUserId = _authService.getCurrentUser()!.uid; + + final QuerySnapshot matchSnapshot = await _firestore + .collection(Constants.dbCollectionUsers) + .doc(swipedUserId) + .collection(Constants.dbCollectionSwipes) + .where('swipedId', isEqualTo: currentUserId) + .where('liked', isEqualTo: true) + .get(); + + if (matchSnapshot.docs.isNotEmpty) { + // save match for both users + final matchesCurrentUser = _firestore + .collection(Constants.dbCollectionUsers) + .doc(currentUserId) + .collection(Constants.dbCollectionMatches); + final matchesSwipedUser = _firestore + .collection(Constants.dbCollectionUsers) + .doc(swipedUserId) + .collection(Constants.dbCollectionMatches); + + await matchesCurrentUser.add({ + 'otherUserId': swipedUserId, + 'timestamp': FieldValue.serverTimestamp(), + }); + + await matchesSwipedUser.add({ + 'otherUserId': currentUserId, + 'timestamp': FieldValue.serverTimestamp(), + }); + + // + // TODO Notification and further logic, e.g. initialization of the chat + // + print( + 'We have a match between ${getUserProfile(currentUserId).name} and ${getUserProfile(swipedUserId).name}'); + + showMatchedScreen(currentUserId, swipedUserId); + } + } + + showMatchedScreen(String currentUserId, String swipedUserId) { + UserProfile currentUser = getUserProfile(currentUserId); + UserProfile swipedUser = getUserProfile(swipedUserId); + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MatchedScreen( + user1Name: swipedUser.name, + user2Name: currentUser.name, + user1ImageUrl: '', // swipedUser.profilePicture, + user2ImageUrl: '', // currentUser.profilePicture, + onMessageButtonPressed: () { + Navigator.pop(context); // Close the MatchedScreen + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChatPage( + receiverEmail: swipedUser.email, + receiverID: swipedUser.uid, + ), + ), + ); + }, + onContinueButtonPressed: () { + Navigator.pop(context); // Close the MatchedScreen + }, + ), + ), + ); } @override @@ -159,21 +252,63 @@ class UserProfilePageState extends State { }, controller: _controller, stackClipBehaviour: Clip.none, + swipeAnchor: SwipeAnchor.bottom, + itemCount: userProfiles.length+1, // +1 for rerun option onSwipeCompleted: (index, direction) { - if (index >= userProfiles.length) { - setState(() { - _controller.currentIndex = 0; // again from the start - }); + // + // Swipe logic goes here + // + String swipedUserId = + userProfiles[_controller.currentIndex].id; + if (direction == SwipeDirection.right) { + _saveSwipeAction(swipedUserId, SwipeDirection.right); + _checkForMatch(swipedUserId); + } else if (direction == SwipeDirection.left) { + _saveSwipeAction(swipedUserId, SwipeDirection.left); } }, 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) { + 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'), + ), + ], + ), + ); + } + final userProfile = userProfiles[properties.index % userProfiles.length]; return Container( alignment: Alignment.center, - color: Colors.tealAccent, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25.0), + color: Colors.blue, + ), child: Stack( children: [ Card( diff --git a/lib/services/chat/chat_service.dart b/lib/services/chat/chat_service.dart index b0fe0f2..625b398 100644 --- a/lib/services/chat/chat_service.dart +++ b/lib/services/chat/chat_service.dart @@ -1,8 +1,10 @@ import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:cofounderella/constants.dart'; -import 'package:cofounderella/models/message.dart'; import 'package:firebase_auth/firebase_auth.dart'; +import '../../constants.dart'; +import '../../helper.dart'; +import '../../models/message.dart'; + class ChatService { // get instance of firestore and auth final FirebaseFirestore _firestore = FirebaseFirestore.instance; @@ -40,10 +42,8 @@ class ChatService { timestamp: timestamp, ); - // construct chat room ID for the two users (sorted to ensure uniqueness) - List ids = [currentUserID, receiverID]; - ids.sort(); // sort to ensure the chatroomID is the same for any 2 users - String chatRoomID = ids.join('_'); + // construct chat room ID for the two users + String chatRoomID = getCompoundId([currentUserID, receiverID]); // add new message to database await _firestore @@ -55,11 +55,7 @@ class ChatService { // get messages Stream getMessages(String userID, otherUserID) { - // TODO create chat room ID -- same code snippet as above - // construct chat room ID for the two users (sorted to ensure uniqueness) - List ids = [userID, otherUserID]; - ids.sort(); // sort to ensure the chatroomID is the same for any 2 users - String chatRoomID = ids.join('_'); + String chatRoomID = getCompoundId([userID, otherUserID]); return _firestore .collection(Constants.dbCollectionChatRooms)