From cad532da0b2ba9b22ac0cdc9771092a8a88d2808 Mon Sep 17 00:00:00 2001 From: Rafael <1024481@stud.hs-mannheim.de> Date: Sun, 2 Jun 2024 16:32:19 +0200 Subject: [PATCH] Added ConversationsPage --- lib/components/my_drawer.dart | 25 ++++++--- lib/pages/conversations_page.dart | 87 +++++++++++++++++++++++++++++ lib/pages/home_page.dart | 30 +++++----- lib/pages/register_page.dart | 7 ++- lib/services/chat/chat_service.dart | 18 +----- lib/services/user_service.dart | 71 ++++++++++++++++++++++- 6 files changed, 194 insertions(+), 44 deletions(-) create mode 100644 lib/pages/conversations_page.dart diff --git a/lib/components/my_drawer.dart b/lib/components/my_drawer.dart index 5b38d05..f70557a 100644 --- a/lib/components/my_drawer.dart +++ b/lib/components/my_drawer.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../pages/conversations_page.dart'; import '../pages/home_page.dart'; import '../pages/user_data_page.dart'; import '../pages/settings_page.dart'; @@ -38,7 +39,7 @@ class MyDrawer extends StatelessWidget { Padding( padding: const EdgeInsets.only(left: 25), child: ListTile( - title: const Text("Home"), + title: const Text('Home'), leading: const Icon(Icons.home), onTap: () { // pop the drawer @@ -58,7 +59,7 @@ class MyDrawer extends StatelessWidget { Padding( padding: const EdgeInsets.only(left: 25), child: ListTile( - title: const Text("Find Matches"), + title: const Text('Find Matches'), leading: const Icon(Icons.person_search), onTap: () { // pop the drawer @@ -79,9 +80,17 @@ class MyDrawer extends StatelessWidget { Padding( padding: const EdgeInsets.only(left: 25), child: ListTile( - title: const Text("Conversations"), + title: const Text('Chats'), leading: const Icon(Icons.chat), - onTap: () {}, // TODO + onTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ConversationsPage(), + ), + ); + }, ), ), @@ -107,7 +116,7 @@ class MyDrawer extends StatelessWidget { Padding( padding: const EdgeInsets.only(left: 25), child: ListTile( - title: const Text("My Profile Settings"), + title: const Text('My Profile Settings'), leading: const Icon(Icons.edit_note), onTap: () { // pop the drawer first, then navigate to destination @@ -156,7 +165,7 @@ class MyDrawer extends StatelessWidget { Padding( padding: const EdgeInsets.only(left: 25), child: ListTile( - title: const Text("About the app"), + title: const Text('About the app'), leading: const Icon(Icons.perm_device_info), onTap: () { Navigator.pop(context); @@ -171,7 +180,7 @@ class MyDrawer extends StatelessWidget { Padding( padding: const EdgeInsets.only(left: 25), child: ListTile( - title: const Text("Send feedback"), + title: const Text('Send feedback'), leading: const Icon(Icons.sentiment_satisfied_alt), onTap: () { showDialog( @@ -187,7 +196,7 @@ class MyDrawer extends StatelessWidget { Padding( padding: const EdgeInsets.only(left: 25, bottom: 25), child: ListTile( - title: const Text("L O G O U T"), + title: const Text('L O G O U T'), leading: const Icon(Icons.logout), onTap: () { Navigator.of(context).pop(); diff --git a/lib/pages/conversations_page.dart b/lib/pages/conversations_page.dart new file mode 100644 index 0000000..66a8f2d --- /dev/null +++ b/lib/pages/conversations_page.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; + +import '../components/user_tile.dart'; +import '../constants.dart'; +import '../services/auth/auth_service.dart'; +import '../services/user_service.dart'; +import 'chat_page.dart'; + +class ConversationsPage extends StatelessWidget { + ConversationsPage({super.key}); + + // auth service + final AuthService _authService = AuthService(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Conversations'), + backgroundColor: Colors.transparent, + foregroundColor: Colors.grey.shade800, + elevation: 0, + ), + body: _buildUserMatchesList(), + ); + } + + // build the list of matches for the current user + Widget _buildUserMatchesList() { + final currentUser = _authService.getCurrentUser(); + + if (currentUser == null) { + return const Center(child: Text('Error: User not logged in')); + } + + return StreamBuilder>>( + stream: UserService.getMatchedUsersStream(currentUser.uid), + builder: (context, snapshot) { + // error + if (snapshot.hasError) { + return Text('Error: ${snapshot.error.toString()}'); + } + + // loading + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + // return list view + if (snapshot.hasData) { + final matchedUsers = snapshot.data!; + if (matchedUsers.isEmpty) { + return const Center(child: Text('No matches yet')); + } + return ListView( + children: matchedUsers + .map( + (userData) => _buildUserListItem(userData, context)) + .toList(), + ); + } else { + return const Center(child: Text('No matches found')); + } + }, + ); + } + + // build individual user list item + Widget _buildUserListItem( + Map userData, BuildContext context) { + return UserTile( + text: userData[Constants.dbFieldUsersEmail], + onTap: () { + // tapped on a user -> go to chat page + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChatPage( + receiverEmail: userData[Constants.dbFieldUsersEmail], + receiverID: userData[Constants.dbFieldUsersID], + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 0787d78..4cb0756 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,23 +1,22 @@ -import 'package:cofounderella/components/my_drawer.dart'; -import 'package:cofounderella/components/user_tile.dart'; -import 'package:cofounderella/services/auth/auth_service.dart'; -import 'package:cofounderella/services/chat/chat_service.dart'; import 'package:flutter/material.dart'; +import '../components/my_drawer.dart'; +import '../components/user_tile.dart'; +import '../constants.dart'; +import '../services/auth/auth_service.dart'; +import '../services/user_service.dart'; import 'chat_page.dart'; class HomePage extends StatelessWidget { HomePage({super.key}); - // chat and auth service - final ChatService _chatService = ChatService(); final AuthService _authService = AuthService(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text("Home"), + title: const Text('Home'), // TODO appbar style: remove background and set elevation to 0 ? backgroundColor: Colors.transparent, foregroundColor: Colors.grey.shade800, @@ -31,16 +30,16 @@ class HomePage extends StatelessWidget { // build a list of users except for the current logged in user Widget _buildUserList() { return StreamBuilder( - stream: _chatService.getUsersStream(), + stream: UserService.getUsersStream(), builder: (context, snapshot) { // error if (snapshot.hasError) { - return const Text("Error"); + return Text('Error: ${snapshot.error.toString()}'); } //loading if (snapshot.connectionState == ConnectionState.waiting) { - return const Text("Loading.."); + return const Text('Loading..'); } // return list view @@ -57,17 +56,18 @@ class HomePage extends StatelessWidget { Widget _buildUserListItem( Map userData, BuildContext context) { // display all users except current user - if (userData["email"] != _authService.getCurrentUser()!.email) { + if (userData[Constants.dbFieldUsersEmail] != + _authService.getCurrentUser()!.email) { return UserTile( - text: userData["email"], + text: userData[Constants.dbFieldUsersEmail], onTap: () { - // tapped on a user -> go to chat page // TODO + // tapped on a user -> go to chat page Navigator.push( context, MaterialPageRoute( builder: (context) => ChatPage( - receiverEmail: userData["email"], - receiverID: userData["uid"], + receiverEmail: userData[Constants.dbFieldUsersEmail], + receiverID: userData[Constants.dbFieldUsersID], ), ), ); diff --git a/lib/pages/register_page.dart b/lib/pages/register_page.dart index 8cff07c..80dbc31 100644 --- a/lib/pages/register_page.dart +++ b/lib/pages/register_page.dart @@ -31,8 +31,11 @@ class RegisterPage extends StatelessWidget { .signUpWithEmailPassword( _emailController.text, _passwordController.text) .then((userCredential) { - UserService.saveUserData(userCredential, _emailController.text, - _nameController.text, _lastnameController.text); + UserService.saveUserRegistrationData( + userCredential, + _emailController.text, + _nameController.text, + _lastnameController.text); }); } on FirebaseAuthException catch (e) { if (context.mounted) { diff --git a/lib/services/chat/chat_service.dart b/lib/services/chat/chat_service.dart index 2aa9adf..311becc 100644 --- a/lib/services/chat/chat_service.dart +++ b/lib/services/chat/chat_service.dart @@ -10,22 +10,6 @@ class ChatService { final FirebaseFirestore _firestore = FirebaseFirestore.instance; final FirebaseAuth _auth = FirebaseAuth.instance; - // get user stream - Stream>> getUsersStream() { - return _firestore - .collection(Constants.dbCollectionUsers) - .snapshots() - .map((snapshot) { - return snapshot.docs.map((doc) { - // iterate each user - final user = doc.data(); - - //return user - return user; - }).toList(); - }); - } - // send message Future sendMessage(String receiverID, message) async { // get current user info @@ -61,7 +45,7 @@ class ChatService { .collection(Constants.dbCollectionChatRooms) .doc(chatRoomID) .collection(Constants.dbCollectionMessages) - .orderBy("timestamp", descending: false) + .orderBy('timestamp', descending: false) .snapshots(); } } diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart index b037a72..b1c796c 100644 --- a/lib/services/user_service.dart +++ b/lib/services/user_service.dart @@ -9,8 +9,8 @@ import '../models/user_profile.dart'; class UserService { UserService._(); // Private constructor to prevent instantiation - static Future saveUserData(UserCredential userCredential, String email, - String firstname, String lastname) async { + static Future saveUserRegistrationData(UserCredential userCredential, + String email, String firstname, String lastname) async { // create full name String fullName = (firstname.isNotEmpty && lastname.isNotEmpty) ? '$firstname $lastname' @@ -151,4 +151,71 @@ class UserService { return userProfile; } + + // get users stream + static Stream>> getUsersStream() { + return FirebaseFirestore.instance + .collection(Constants.dbCollectionUsers) + .snapshots() + .map((snapshot) { + return snapshot.docs.map((doc) { + // iterate each user + final user = doc.data(); + + //return user + return user; + }).toList(); + }); + } + + // Get list of matched user ids for a given user + static Future> getMatchedUserIds(String userId) async { + List matchedUserIds = []; + + final snapshot = await FirebaseFirestore.instance + .collection(Constants.dbCollectionUsers) + .doc(userId) + .collection(Constants.dbCollectionMatches) + .get(); + + for (var doc in snapshot.docs) { + final data = doc.data(); + final otherUserId = data['otherUserId'] as String?; + if (otherUserId != null && otherUserId.isNotEmpty) { + matchedUserIds.add(otherUserId); + } + // assuming the match document ID is the matched user's ID + // it would just be an one liner: + // matchedUserIds.add(doc.id); + } + + return matchedUserIds; + } + + // Get matched users data for a given user as stream + static Stream>> getMatchedUsersStream( + String userId) { + return FirebaseFirestore.instance + .collection(Constants.dbCollectionUsers) + .doc(userId) + .collection(Constants.dbCollectionMatches) + .snapshots() + .asyncMap((snapshot) async { + final matchedUserIds = snapshot.docs + .map((doc) => doc.data()['otherUserId'] as String?) + .where((otherUserId) => otherUserId != null && otherUserId.isNotEmpty) + .toList(); + + if (matchedUserIds.isEmpty) { + return []; + } + + final matchedUsersSnapshot = await FirebaseFirestore.instance + .collection(Constants.dbCollectionUsers) + .where(FieldPath.documentId, whereIn: matchedUserIds) + .get(); + + return matchedUsersSnapshot.docs.map((doc) => doc.data()).toList(); + }); + } }