Updated swiping page

master
Rafael 2024-05-26 01:44:49 +02:00
parent 9c25f2402d
commit f38493fc79
3 changed files with 251 additions and 144 deletions

View File

@ -8,7 +8,8 @@ class MatchedScreen extends StatelessWidget {
final VoidCallback onMessageButtonPressed; final VoidCallback onMessageButtonPressed;
final VoidCallback onContinueButtonPressed; final VoidCallback onContinueButtonPressed;
const MatchedScreen({super.key, const MatchedScreen({
super.key,
required this.user1Name, required this.user1Name,
required this.user2Name, required this.user2Name,
required this.user1ImageUrl, required this.user1ImageUrl,
@ -35,13 +36,20 @@ class MatchedScreen extends StatelessWidget {
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// TODO imageUrl cant be null or empty with NetworkImage
CircleAvatar( CircleAvatar(
backgroundImage: NetworkImage(user1ImageUrl), backgroundColor: Colors.blueGrey[300],
backgroundImage: (user1ImageUrl.isEmpty)
? null
: NetworkImage(user1ImageUrl),
radius: 50, radius: 50,
), ),
const SizedBox(width: 24), const SizedBox(width: 24),
CircleAvatar( CircleAvatar(
backgroundImage: NetworkImage(user2ImageUrl), backgroundColor: Colors.blueGrey[300],
backgroundImage: (user2ImageUrl.isEmpty)
? null
: NetworkImage(user2ImageUrl),
radius: 50, radius: 50,
), ),
], ],

View File

@ -1,10 +1,11 @@
import 'package:cloud_firestore/cloud_firestore.dart'; 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 '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 { class ChatPage extends StatefulWidget {
final String receiverEmail; final String receiverEmail;
final String receiverID; final String receiverID;
@ -152,7 +153,7 @@ class _ChatPageState extends State<ChatPage> {
), ),
), ),
ChatBubble( ChatBubble(
message: data["message"], message: data['message'],
isCurrentUser: isCurrentUser, isCurrentUser: isCurrentUser,
), ),
], ],
@ -169,8 +170,8 @@ class _ChatPageState extends State<ChatPage> {
Expanded( Expanded(
child: MyTextField( child: MyTextField(
controller: _messageController, controller: _messageController,
hintText: "Type a message", hintText: 'Type a message',
obscureText: false, // TODO make this optional obscureText: false,
focusNode: myFocusNode, focusNode: myFocusNode,
), ),
), ),

View File

@ -19,7 +19,15 @@ class UserProfilePage extends StatefulWidget {
} }
class UserProfilePageState extends State<UserProfilePage> { class UserProfilePageState extends State<UserProfilePage> {
/// List with [all] user profiles
List<UserProfile> userProfiles = []; List<UserProfile> userProfiles = [];
/// The current's user profile
UserProfile? currentUserProfile;
/// Other users that yet did either not receive a swipe status
List<UserProfile> potentialUserProfiles = []; // potential users for matching
late final SwipableStackController _controller; late final SwipableStackController _controller;
// get instance of firestore and auth // get instance of firestore and auth
@ -44,14 +52,32 @@ class UserProfilePageState extends State<UserProfilePage> {
} }
Future<void> _fetchUserProfiles() async { Future<void> _fetchUserProfiles() async {
final querySnapshot = await FirebaseFirestore.instance final usersSnapshot = await FirebaseFirestore.instance
.collection(Constants.dbCollectionUsers) .collection(Constants.dbCollectionUsers)
.get(); .get();
final users = await Future.wait(querySnapshot.docs.map((doc) async { final String currentUserId = _authService.getCurrentUser()!.uid;
final languagesSnapshot =
await doc.reference.collection(Constants.dbCollectionLanguages).get(); // Fetch the list of profiles the current user has already swiped
final locationsSnapshot = final QuerySnapshot swipesSnapshot = await FirebaseFirestore.instance
await doc.reference.collection(Constants.dbCollectionLocations).get(); .collection(Constants.dbCollectionUsers)
.doc(currentUserId)
.collection(Constants.dbCollectionSwipes)
.get();
final Set<String> likedUserIds = swipesSnapshot.docs
.where((doc) => doc['liked'] == true)
.map((doc) => doc.id)
.toSet();
List<UserProfile> allUsers = [];
List<UserProfile> 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 languages = languagesSnapshot.docs.map((doc) {
final data = doc.data(); final data = doc.data();
@ -63,13 +89,10 @@ class UserProfilePageState extends State<UserProfilePage> {
); );
}).toList(); }).toList();
final mainDoc = locationsSnapshot.docs.firstWhereOrNull( final mainDoc = locationsSnapshot.docs
(doc) => doc.id == Constants.dbDocMainLocation, .firstWhereOrNull((doc) => doc.id == Constants.dbDocMainLocation);
); final secondaryDoc = locationsSnapshot.docs
final secondaryDoc = locationsSnapshot.docs.firstWhereOrNull( .firstWhereOrNull((doc) => doc.id == Constants.dbDocSecondLocation);
(doc) => doc.id == Constants.dbDocSecondLocation,
);
final locations = { final locations = {
Constants.dbDocMainLocation: Constants.dbDocMainLocation:
mainDoc != null ? _createLocationFromDoc(mainDoc.data()) : null, mainDoc != null ? _createLocationFromDoc(mainDoc.data()) : null,
@ -78,9 +101,9 @@ class UserProfilePageState extends State<UserProfilePage> {
: null, : null,
}; };
final data = doc.data(); final data = userDoc.data();
return UserProfile( UserProfile userProfile = UserProfile(
id: doc.id, id: userDoc.id,
uid: data[Constants.dbFieldUsersID] ?? '', uid: data[Constants.dbFieldUsersID] ?? '',
email: data[Constants.dbFieldUsersEmail] ?? '', email: data[Constants.dbFieldUsersEmail] ?? '',
name: data[Constants.dbFieldUsersName] ?? '', name: data[Constants.dbFieldUsersName] ?? '',
@ -93,11 +116,21 @@ class UserProfilePageState extends State<UserProfilePage> {
languages: languages, languages: languages,
locations: locations, locations: locations,
); );
}).toList());
setState(() { // add profiles accordingly
userProfiles = users; 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<String, dynamic>? data) { MyLocation? _createLocationFromDoc(Map<String, dynamic>? data) {
@ -117,7 +150,7 @@ class UserProfilePageState extends State<UserProfilePage> {
void _swipeLeft() { void _swipeLeft() {
_controller.next( _controller.next(
swipeDirection: SwipeDirection.left, duration: Durations.extralong4); swipeDirection: SwipeDirection.left, duration: Durations.extralong1);
} }
void _swipeRight() { void _swipeRight() {
@ -127,7 +160,7 @@ class UserProfilePageState extends State<UserProfilePage> {
void _skip() { void _skip() {
_controller.next( _controller.next(
swipeDirection: SwipeDirection.up, duration: Durations.extralong2); swipeDirection: SwipeDirection.up, duration: Durations.long4);
} }
/// Save swipe status to database /// Save swipe status to database
@ -151,7 +184,7 @@ class UserProfilePageState extends State<UserProfilePage> {
return userProfiles.firstWhere((x) => x.uid == userId); 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<void> _checkForMatch(swipedUserId) async { Future<void> _checkForMatch(swipedUserId) async {
String currentUserId = _authService.getCurrentUser()!.uid; String currentUserId = _authService.getCurrentUser()!.uid;
@ -185,11 +218,8 @@ class UserProfilePageState extends State<UserProfilePage> {
}); });
// //
// 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); showMatchedScreen(currentUserId, swipedUserId);
} }
} }
@ -228,10 +258,32 @@ class UserProfilePageState extends State<UserProfilePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (userProfiles.isEmpty) { if (potentialUserProfiles.isEmpty) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('User Profiles')), 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<UserProfilePage> {
controller: _controller, controller: _controller,
stackClipBehaviour: Clip.none, stackClipBehaviour: Clip.none,
swipeAnchor: SwipeAnchor.bottom, 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) { onSwipeCompleted: (index, direction) {
// //
// Swipe logic goes here // Swipe logic
// //
String swipedUserId = String swipedUserId =
userProfiles[_controller.currentIndex].id; potentialUserProfiles[_controller.currentIndex].id;
if (direction == SwipeDirection.right) { if (direction == SwipeDirection.right) {
_saveSwipeAction(swipedUserId, SwipeDirection.right); _saveSwipeAction(swipedUserId, SwipeDirection.right);
_checkForMatch(swipedUserId); _checkForMatch(swipedUserId);
@ -269,40 +322,15 @@ class UserProfilePageState extends State<UserProfilePage> {
}, },
horizontalSwipeThreshold: 0.8, horizontalSwipeThreshold: 0.8,
verticalSwipeThreshold: 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) { builder: (context, properties) {
if (properties.index == potentialUserProfiles.length) {
if (properties.index == userProfiles.length) {
return Center( return Center(
child: Column( // Show end message and restart button child: _buildLastCard(), // last card
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 = final userProfile = potentialUserProfiles[
userProfiles[properties.index % userProfiles.length]; properties.index % potentialUserProfiles.length];
return Container( return Container(
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -311,34 +339,7 @@ class UserProfilePageState extends State<UserProfilePage> {
), ),
child: Stack( child: Stack(
children: [ children: [
Card( _buildUserCard(userProfile),
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'}'),
],
),
),
),
),
if (properties.stackIndex == 0 && if (properties.stackIndex == 0 &&
properties.direction != null) properties.direction != null)
CardOverlay( CardOverlay(
@ -352,55 +353,152 @@ class UserProfilePageState extends State<UserProfilePage> {
), ),
), ),
), ),
Positioned( _buildSwipeButtons(),
bottom: 16, ],
left: 0, ),
right: 0, ),
child: Row( );
mainAxisAlignment: MainAxisAlignment.spaceEvenly, }
children: [
FloatingActionButton( Widget _buildUserCard(UserProfile userProfile) {
tooltip: 'Undo last action', return Card(
shape: const CircleBorder(), child: Padding(
onPressed: () { padding: const EdgeInsets.all(16.0),
_controller.rewind(duration: Durations.extralong4); child: SingleChildScrollView(
}, child: Column(
child: const Icon(Icons.undo), mainAxisSize: MainAxisSize.min,
), children: [
SizedBox( Text(userProfile.name, style: const TextStyle(fontSize: 24)),
width: 72, Text(userProfile.email, style: const TextStyle(fontSize: 24)),
height: 72, const SizedBox(height: 8),
child: FloatingActionButton( Text(
shape: const CircleBorder(), 'Has skills and experience in: ${userProfile.skills.join(', ')}'),
onPressed: _swipeLeft, Text(
child: 'Seeks someone with skills in: ${userProfile.skillsSought.join(', ')}'),
const Icon(Icons.cancel, color: Colors.red, size: 64), Text('Risk type: ${userProfile.risk}'),
), Text(
), 'Speaks: ${userProfile.languages.map((lang) => lang.name).join(', ')}'),
SizedBox( Text(
width: 72, 'Lives in: ${userProfile.locations[Constants.dbDocMainLocation]?.locality ?? 'N/A'}'),
height: 72, Text(
child: FloatingActionButton( 'Second home: ${userProfile.locations[Constants.dbDocSecondLocation]?.locality ?? 'N/A'}'),
shape: const CircleBorder(), ],
onPressed: _swipeRight, ),
child: const Icon(Icons.check_circle, ),
color: Colors.green, size: 64), ),
), );
), }
FloatingActionButton(
tooltip: 'Skip profile', Widget _buildLastCard() {
shape: const CircleBorder(), return Padding(
onPressed: _skip, padding: const EdgeInsets.all(16.0),
child: const Icon(Icons.skip_next), 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 { class CardOverlay extends StatelessWidget {
@ -427,9 +525,9 @@ class CardOverlay extends StatelessWidget {
: Alignment.center), : Alignment.center),
child: Icon( child: Icon(
direction == SwipeDirection.right direction == SwipeDirection.right
? Icons.check_circle ? Icons.thumb_up
: (direction == SwipeDirection.left : (direction == SwipeDirection.left
? Icons.cancel ? Icons.thumb_down
: Icons.skip_next), : Icons.skip_next),
size: 100, size: 100,
color: direction == SwipeDirection.right color: direction == SwipeDirection.right