diff --git a/lib/components/my_drawer.dart b/lib/components/my_drawer.dart index 560b53a..aecc90f 100644 --- a/lib/components/my_drawer.dart +++ b/lib/components/my_drawer.dart @@ -116,29 +116,10 @@ class MyDrawer extends StatelessWidget { ), ), - // settings list tile Padding( padding: const EdgeInsets.only(left: 25), child: ListTile( - title: const Text("My Profile"), - leading: const Icon(Icons.settings), - onTap: () { - // pop the drawer and navigate to settings page - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SettingsPage(), - ), - ); - }, - ), - ), - - Padding( - padding: const EdgeInsets.only(left: 25), - child: ListTile( - title: const Text('My Profile Settings'), + title: const Text('My Profile'), leading: const Icon(Icons.edit_note), onTap: () { // pop the drawer first, then navigate to destination @@ -153,6 +134,25 @@ class MyDrawer extends StatelessWidget { ), ), + // settings list tile + Padding( + padding: const EdgeInsets.only(left: 25), + child: ListTile( + title: const Text("App Settings"), + leading: const Icon(Icons.settings), + onTap: () { + // pop the drawer and navigate to settings page + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SettingsPage(), + ), + ); + }, + ), + ), + // TODO TESTING - user data tile Padding( padding: const EdgeInsets.only(left: 25), diff --git a/lib/components/user_tile.dart b/lib/components/user_tile.dart index aff7e57..d224a38 100644 --- a/lib/components/user_tile.dart +++ b/lib/components/user_tile.dart @@ -1,21 +1,13 @@ import 'package:flutter/material.dart'; -import '../models/message.dart'; -import '../services/chat/chat_service.dart'; -import '../utils/helper.dart'; - class UserTile extends StatelessWidget { - final String headerText; - final String? currentUserId; - final String? otherUserId; + final String text; final String? profileImageUrl; final void Function()? onTap; const UserTile({ super.key, - required this.headerText, - this.currentUserId, - this.otherUserId, + required this.text, this.profileImageUrl, required this.onTap, }); @@ -30,110 +22,26 @@ class UserTile extends StatelessWidget { borderRadius: BorderRadius.circular(12), ), margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 25), - padding: const EdgeInsets.all(10), + padding: const EdgeInsets.all(20), child: Row( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - buildProfileIcon(), - buildMsgContent(), + // Profile image + if (profileImageUrl != null && profileImageUrl!.isNotEmpty) + CircleAvatar( + backgroundImage: NetworkImage(profileImageUrl!), + radius: 24, + ), + // Icon if profile image is not set + if (profileImageUrl == null || profileImageUrl!.isEmpty) + const Icon(Icons.person), + + const SizedBox(width: 20), + + // user name + Text(text), ], ), ), ); } - - Widget buildProfileIcon() { - return Row( - children: [ - // Profile image - if (profileImageUrl != null && profileImageUrl!.isNotEmpty) - CircleAvatar( - backgroundImage: NetworkImage(profileImageUrl!), - radius: 24, - ), - // Icon if profile image is not set - if (profileImageUrl == null || profileImageUrl!.isEmpty) - const Icon(Icons.person), - - const SizedBox(width: 12), - ], - ); - } - - Widget buildMsgContent() { - String msgDateString = ''; - String msgContent = ''; - bool? outgoing; - String chatRoomID = getCompoundId([currentUserId ?? '', otherUserId ?? '']); - - return FutureBuilder( - future: ChatService().getLastMessage(chatRoomID), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const CircularProgressIndicator(); - } else if (snapshot.hasError) { - return Text( - 'Error: ${snapshot.error}', - style: const TextStyle(color: Colors.red), - ); - } else if (!snapshot.hasData || snapshot.data == null) { - return const Text('No messages yet'); - } else { - Message lastMessage = snapshot.data!; - msgDateString = formatTimestamp(lastMessage.timestamp); - - if (lastMessage.senderID == currentUserId) { - msgContent = 'Me: ${lastMessage.message}'; - outgoing = true; - } else if (lastMessage.senderID == otherUserId) { - msgContent = lastMessage.message; - outgoing = false; - } - - return Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - // user name - Text( - headerText, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 4), - Text(msgContent, overflow: TextOverflow.ellipsis, maxLines: 1), - Row( - children: [ - if (outgoing == true) - const Icon( - Icons.call_made, - color: Colors.green, - size: 16, - ), - if (outgoing == false) - const Icon( - Icons.call_received, - color: Colors.blue, - size: 16, - ), - const SizedBox(width: 4), - Flexible( - child: Text( - msgDateString, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - ], - ), - ], - ), - ); - } - }, - ); - } } diff --git a/lib/components/user_tile_chats.dart b/lib/components/user_tile_chats.dart new file mode 100644 index 0000000..5e21bfc --- /dev/null +++ b/lib/components/user_tile_chats.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; + +import '../models/message.dart'; +import '../services/chat/chat_service.dart'; +import '../utils/helper.dart'; + +class UserTileChats extends StatelessWidget { + final String headerText; + final String? currentUserId; + final String? otherUserId; + final String? profileImageUrl; + final void Function()? onTap; + + const UserTileChats({ + super.key, + required this.headerText, + this.currentUserId, + this.otherUserId, + this.profileImageUrl, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + borderRadius: BorderRadius.circular(12), + ), + margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 25), + padding: const EdgeInsets.all(10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildProfileIcon(), + buildMsgContent(), + ], + ), + ), + ); + } + + Widget buildProfileIcon() { + return Row( + children: [ + // Profile image + if (profileImageUrl != null && profileImageUrl!.isNotEmpty) + CircleAvatar( + backgroundImage: NetworkImage(profileImageUrl!), + radius: 24, + ), + // Icon if profile image is not set + if (profileImageUrl == null || profileImageUrl!.isEmpty) + const Icon(Icons.person), + + const SizedBox(width: 12), + ], + ); + } + + Widget buildMsgContent() { + String msgDateString = ''; + String msgContent = ''; + bool? outgoing; + String chatRoomID = getCompoundId([currentUserId ?? '', otherUserId ?? '']); + + return FutureBuilder( + future: ChatService().getLastMessage(chatRoomID), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const CircularProgressIndicator(); + } else if (snapshot.hasError) { + return Text( + 'Error: ${snapshot.error}', + style: const TextStyle(color: Colors.red), + ); + } else if (!snapshot.hasData || snapshot.data == null) { + return const Text('No messages yet'); + } else { + Message lastMessage = snapshot.data!; + msgDateString = formatTimestamp(lastMessage.timestamp); + + if (lastMessage.senderID == currentUserId) { + msgContent = 'Me: ${lastMessage.message}'; + outgoing = true; + } else if (lastMessage.senderID == otherUserId) { + msgContent = lastMessage.message; + outgoing = false; + } + + return Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // user name + Text( + headerText, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text(msgContent, overflow: TextOverflow.ellipsis, maxLines: 1), + Row( + children: [ + if (outgoing == true) + const Icon( + Icons.call_made, + color: Colors.green, + size: 16, + ), + if (outgoing == false) + const Icon( + Icons.call_received, + color: Colors.blue, + size: 16, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + msgDateString, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ], + ), + ); + } + }, + ); + } +} diff --git a/lib/components/user_tile_likes.dart b/lib/components/user_tile_likes.dart new file mode 100644 index 0000000..f0a6903 --- /dev/null +++ b/lib/components/user_tile_likes.dart @@ -0,0 +1,74 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; + +import '../constants.dart'; + +class UserTileLikes extends StatelessWidget { + final DocumentSnapshot user; + final bool hasMatch; + final VoidCallback onUnlike; + final VoidCallback onShowMatchMessage; + final VoidCallback onViewInfo; + + const UserTileLikes({ + super.key, + required this.user, + required this.hasMatch, + required this.onUnlike, + required this.onShowMatchMessage, + required this.onViewInfo, + }); + + @override + Widget build(BuildContext context) { + Map userMap = user.data() as Map; + bool hasPictureUrl = userMap.containsKey(Constants.dbFieldUsersProfilePic); + bool hasName = userMap.containsKey(Constants.dbFieldUsersName); + bool hasBio = userMap.containsKey(Constants.dbFieldUsersBio); + + return Card( + margin: const EdgeInsets.all(8.0), + child: ListTile( + leading: hasPictureUrl == true && + user[Constants.dbFieldUsersProfilePic] != null + ? CircleAvatar( + backgroundImage: NetworkImage( + user[Constants.dbFieldUsersProfilePic], + ), + ) + : const CircleAvatar( + child: Icon(Icons.person), + ), + title: hasName + ? Text( + '${user[Constants.dbFieldUsersName]}', + overflow: TextOverflow.ellipsis, + maxLines: 1, + ) + : null, + subtitle: hasBio + ? Text( + user[Constants.dbFieldUsersBio], + overflow: TextOverflow.ellipsis, + maxLines: 3, + ) + : null, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.contact_page_outlined), + onPressed: onViewInfo, + ), + IconButton( + icon: hasMatch + ? const Icon(Icons.lock_outline) + : const Icon(Icons.delete_outline, color: Colors.red), + onPressed: hasMatch ? onShowMatchMessage : onUnlike, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/conversations_page.dart b/lib/pages/conversations_page.dart index e45a17b..0001703 100644 --- a/lib/pages/conversations_page.dart +++ b/lib/pages/conversations_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../components/user_tile.dart'; +import '../components/user_tile_chats.dart'; import '../constants.dart'; import '../services/auth/auth_service.dart'; import '../services/user_service.dart'; @@ -67,7 +67,7 @@ class ConversationsPage extends StatelessWidget { // build individual user list item Widget _buildUserListItem(String currentUserId, Map userData, BuildContext context) { - return UserTile( + return UserTileChats( headerText: userData[Constants.dbFieldUsersName], currentUserId: currentUserId, otherUserId: userData[Constants.dbFieldUsersID], diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 182f608..4cb0756 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -59,7 +59,7 @@ class HomePage extends StatelessWidget { if (userData[Constants.dbFieldUsersEmail] != _authService.getCurrentUser()!.email) { return UserTile( - headerText: userData[Constants.dbFieldUsersEmail], + text: userData[Constants.dbFieldUsersEmail], onTap: () { // tapped on a user -> go to chat page Navigator.push( diff --git a/lib/pages/liked_users_page.dart b/lib/pages/liked_users_page.dart index e863496..cebf118 100644 --- a/lib/pages/liked_users_page.dart +++ b/lib/pages/liked_users_page.dart @@ -1,10 +1,15 @@ import 'package:flutter/material.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; +import '../components/user_tile_likes.dart'; import '../constants.dart'; import '../services/auth/auth_service.dart'; import '../utils/helper.dart'; +enum MenuSort { nameAsc, nameDesc, timestampAsc, timestampDesc } + +enum ViewOrder { swipedFirst, matchedFirst } + class LikedUsersPage extends StatefulWidget { const LikedUsersPage({super.key}); @@ -15,6 +20,9 @@ class LikedUsersPage extends StatefulWidget { class LikedUsersPageState extends State { final String currentUserId = AuthService().getCurrentUser()!.uid; + ViewOrder _orderPreference = ViewOrder.swipedFirst; + MenuSort _sortPreference = MenuSort.nameAsc; + Future> _fetchLikedUsers() async { QuerySnapshot likedUsersSnapshot = await FirebaseFirestore.instance .collection(Constants.dbCollectionUsers) @@ -134,14 +142,119 @@ class LikedUsersPageState extends State { ); } + Future> _fetchSortedLikedUsers() async { + List likedUsers = await _fetchLikedUsers(); + List likedOnlyUsers = []; + List matchedUsers = []; + + for (DocumentSnapshot user in likedUsers) { + bool hasMatch = await _hasMatch(user.id); + if (hasMatch) { + matchedUsers.add(user); + } else { + likedOnlyUsers.add(user); + } + } + + if (_sortPreference == MenuSort.nameAsc) { + likedOnlyUsers.sort((a, b) => (a[Constants.dbFieldUsersName] as String) + .compareTo(b[Constants.dbFieldUsersName] as String)); + matchedUsers.sort((a, b) => (a[Constants.dbFieldUsersName] as String) + .compareTo(b[Constants.dbFieldUsersName] as String)); + } else if (_sortPreference == MenuSort.nameDesc) { + likedOnlyUsers.sort((a, b) => (b[Constants.dbFieldUsersName] as String) + .compareTo(a[Constants.dbFieldUsersName] as String)); + matchedUsers.sort((a, b) => (b[Constants.dbFieldUsersName] as String) + .compareTo(a[Constants.dbFieldUsersName] as String)); + } + + if (_orderPreference == ViewOrder.swipedFirst) { + return [...likedOnlyUsers, ...matchedUsers]; + } else { + return [...matchedUsers, ...likedOnlyUsers]; + } + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Liked Users'), ), - body: FutureBuilder>( - future: _fetchLikedUsers(), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + DropdownButton( + value: _orderPreference, + items: const [ + DropdownMenuItem( + value: ViewOrder.swipedFirst, + child: Text('Swiped First')), + DropdownMenuItem( + value: ViewOrder.matchedFirst, + child: Text('Matched First')), + ], + onChanged: (value) { + // update UI on change only + if (_orderPreference != value) { + setState(() { + _orderPreference = value!; + }); + } + }, + ), + Align( + alignment: Alignment.centerRight, + child: PopupMenuButton( + icon: const Icon(Icons.sort), + onSelected: (MenuSort item) { + // update UI on change only + if (_sortPreference != item) { + setState(() { + _sortPreference = item; + }); + } + }, + itemBuilder: (BuildContext context) => + >[ + PopupMenuItem( + value: MenuSort.nameAsc, + child: ListTile( + leading: _sortPreference == MenuSort.nameAsc + ? const Icon(Icons.check) + : const Icon(null), + title: const Text('Name Ascending'), + ), + ), + PopupMenuItem( + value: MenuSort.nameDesc, + child: ListTile( + leading: _sortPreference == MenuSort.nameDesc + ? const Icon(Icons.check) + : const Icon(null), + title: const Text('Name Descending'), + ), + ), + ], + ), + ), + ], + ), + ), + buildLikedUserList(), + ], + ), + ); + } + + Widget buildLikedUserList() { + return Expanded( + child: FutureBuilder>( + future: _fetchSortedLikedUsers(), builder: (BuildContext context, AsyncSnapshot> snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { @@ -162,7 +275,7 @@ class LikedUsersPageState extends State { return const CircularProgressIndicator(); } bool hasMatch = snapshot.data ?? false; - return UserInfoTile( + return UserTileLikes( user: user, hasMatch: hasMatch, onUnlike: () { @@ -191,68 +304,3 @@ class LikedUsersPageState extends State { ); } } - -class UserInfoTile extends StatelessWidget { - final DocumentSnapshot user; - final bool hasMatch; - final VoidCallback onUnlike; - final VoidCallback onShowMatchMessage; - final VoidCallback onViewInfo; - - const UserInfoTile({ - super.key, - required this.user, - required this.hasMatch, - required this.onUnlike, - required this.onShowMatchMessage, - required this.onViewInfo, - }); - - @override - Widget build(BuildContext context) { - Map userMap = user.data() as Map; - bool hasPictureUrl = userMap.containsKey(Constants.dbFieldUsersProfilePic); - bool hasName = userMap.containsKey(Constants.dbFieldUsersName); - bool hasBio = userMap.containsKey(Constants.dbFieldUsersBio); - - return Card( - margin: const EdgeInsets.all(8.0), - child: ListTile( - leading: hasPictureUrl == true && - user[Constants.dbFieldUsersProfilePic] != null - ? CircleAvatar( - backgroundImage: - NetworkImage(user[Constants.dbFieldUsersProfilePic]), - ) - : const CircleAvatar(child: Icon(Icons.person)), - title: hasName - ? Text( - '${user[Constants.dbFieldUsersName]} ${user[Constants.dbFieldUsersName]} ${user[Constants.dbFieldUsersName]}', - overflow: TextOverflow.ellipsis, - maxLines: 1, - ) - : null, - subtitle: hasBio - ? Text( - user[Constants.dbFieldUsersBio], - overflow: TextOverflow.ellipsis, - maxLines: 3, - ) - : null, - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.contact_page_outlined), - onPressed: onViewInfo, - ), - IconButton( - icon: Icon(hasMatch ? Icons.lock_outline : Icons.delete_outline), - onPressed: hasMatch ? onShowMatchMessage : onUnlike, - ), - ], - ), - ), - ); - } -}