From 25b76e6274ff5737ee639679d294873713bea12f Mon Sep 17 00:00:00 2001 From: Rafael <1024481@stud.hs-mannheim.de> Date: Thu, 6 Jun 2024 16:11:26 +0200 Subject: [PATCH] LikedUsersPage: Sort View (2) --- lib/constants.dart | 4 + lib/enumerations.dart | 14 ++ lib/models/swipe.dart | 25 +++ lib/pages/liked_users_page.dart | 277 ++++++++++++++++++++------------ 4 files changed, 221 insertions(+), 99 deletions(-) create mode 100644 lib/models/swipe.dart diff --git a/lib/constants.dart b/lib/constants.dart index ea6539d..67668cf 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -33,6 +33,10 @@ class Constants { static const String dbFieldMessageSenderEmail = 'senderEmail'; static const String dbFieldMessageTimestamp = 'timestamp'; + static const String dbFieldSwipesSwipedId = 'swipedId'; + static const String dbFieldSwipesLike = 'liked'; + static const String dbFieldSwipesTimestamp = 'timestamp'; + static const String dbFieldUsersID = 'uid'; static const String dbFieldUsersEmail = 'email'; static const String dbFieldUsersName = 'name'; diff --git a/lib/enumerations.dart b/lib/enumerations.dart index a64b1e0..747e6be 100644 --- a/lib/enumerations.dart +++ b/lib/enumerations.dart @@ -1,3 +1,17 @@ +enum MenuSort { + nameAsc, + nameDesc, + timestampAsc, + timestampDesc, +} + +enum ViewOrder { + swipedFirst, + matchedFirst, + swipedOnly, + matchedOnly, +} + enum Gender { none, male, diff --git a/lib/models/swipe.dart b/lib/models/swipe.dart new file mode 100644 index 0000000..7a5a74f --- /dev/null +++ b/lib/models/swipe.dart @@ -0,0 +1,25 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import '../constants.dart'; + +class Swipe { + final String swiperId; + final String swipedId; + final bool liked; + final DateTime timestamp; + + Swipe( + {required this.swiperId, + required this.swipedId, + required this.liked, + required this.timestamp}); + + factory Swipe.fromDocument(DocumentSnapshot doc) { + var data = doc.data() as Map; + return Swipe( + swiperId: doc.id, // Assign document ID + swipedId: data[Constants.dbFieldSwipesSwipedId], + liked: data[Constants.dbFieldSwipesLike] as bool, + timestamp: data[Constants.dbFieldSwipesTimestamp].toDate(), + ); + } +} diff --git a/lib/pages/liked_users_page.dart b/lib/pages/liked_users_page.dart index cebf118..3acbcd9 100644 --- a/lib/pages/liked_users_page.dart +++ b/lib/pages/liked_users_page.dart @@ -3,13 +3,11 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import '../components/user_tile_likes.dart'; import '../constants.dart'; +import '../enumerations.dart'; +import '../models/swipe.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}); @@ -19,31 +17,55 @@ class LikedUsersPage extends StatefulWidget { class LikedUsersPageState extends State { final String currentUserId = AuthService().getCurrentUser()!.uid; + final Map _cachedMatches = {}; + List> _likedUsersWithSwipes = []; + bool _isLoading = true; + String? _fetchError; ViewOrder _orderPreference = ViewOrder.swipedFirst; MenuSort _sortPreference = MenuSort.nameAsc; - Future> _fetchLikedUsers() async { - QuerySnapshot likedUsersSnapshot = await FirebaseFirestore.instance - .collection(Constants.dbCollectionUsers) - .doc(currentUserId) - .collection(Constants.dbCollectionSwipes) - .where('liked', isEqualTo: true) - .get(); + @override + void initState() { + super.initState(); + _fetchLikedUsersWithSwipes(); + } - List likedUserIds = - likedUsersSnapshot.docs.map((doc) => doc.id).toList(); - - List likedUsers = []; - for (String userId in likedUserIds) { - DocumentSnapshot userDoc = await FirebaseFirestore.instance + Future _fetchLikedUsersWithSwipes() async { + try { + QuerySnapshot likedUsersSnapshot = await FirebaseFirestore.instance .collection(Constants.dbCollectionUsers) - .doc(userId) + .doc(currentUserId) + .collection(Constants.dbCollectionSwipes) + .where(Constants.dbFieldSwipesLike, isEqualTo: true) .get(); - likedUsers.add(userDoc); - } - return likedUsers; + List> likedUsersWithSwipes = []; + + for (var doc in likedUsersSnapshot.docs) { + Swipe swipe = Swipe.fromDocument(doc); + DocumentSnapshot userDoc = await FirebaseFirestore.instance + .collection(Constants.dbCollectionUsers) + .doc(swipe.swipedId) + .get(); + + // Initialize _cachedMatches to keep database accesses to a minimum + bool hasMatch = await _hasMatch(swipe.swipedId); + _cachedMatches[swipe.swipedId] = hasMatch; + + likedUsersWithSwipes.add(MapEntry(swipe, userDoc)); + } + + setState(() { + _likedUsersWithSwipes = likedUsersWithSwipes; + _isLoading = false; + }); + } catch (e) { + setState(() { + _fetchError = e.toString(); + _isLoading = false; + }); + } } Future _hasMatch(String userId) async { @@ -63,10 +85,17 @@ class LikedUsersPageState extends State { .doc(currentUserId) .collection(Constants.dbCollectionSwipes) .doc(userId) - .delete(); //.update({'liked': false}); - setState(() {}); // Refresh the UI + .delete(); + + // Refresh the UI + setState(() { + _likedUsersWithSwipes + .removeWhere((entry) => entry.key.swipedId == userId); + }); } catch (e) { - showMsg(context, 'Error unlike', e.toString()); + if (mounted) { + showMsg(context, 'Error during unlike', e.toString()); + } } } @@ -90,7 +119,7 @@ class LikedUsersPageState extends State { ); } - Future _showConfirmationDialog(String userId, String userName) async { + Future _showConfirmationDialog(String userId, String userName) async { bool? confirm = await showDialog( context: context, builder: (BuildContext context) { @@ -111,9 +140,7 @@ class LikedUsersPageState extends State { }, ); - if (confirm == true) { - await _unlikeUser(userId); - } + return confirm; } void _showUserInfo(DocumentSnapshot user) { @@ -142,39 +169,65 @@ class LikedUsersPageState extends State { ); } - Future> _fetchSortedLikedUsers() async { - List likedUsers = await _fetchLikedUsers(); - List likedOnlyUsers = []; - List matchedUsers = []; + List> _getSortedLikedUsersWithSwipes() { + List> likedOnlyUsers = []; + List> matchedUsers = []; + + for (var entry in _likedUsersWithSwipes) { + bool hasMatch = _cachedMatches.containsKey(entry.key.swipedId) + ? _cachedMatches[entry.key.swipedId]! + : false; - for (DocumentSnapshot user in likedUsers) { - bool hasMatch = await _hasMatch(user.id); if (hasMatch) { - matchedUsers.add(user); + matchedUsers.add(entry); } else { - likedOnlyUsers.add(user); + likedOnlyUsers.add(entry); } } - 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)); + if (_sortPreference == MenuSort.timestampAsc) { + likedOnlyUsers.sort((a, b) => a.key.timestamp.compareTo(b.key.timestamp)); + matchedUsers.sort((a, b) => a.key.timestamp.compareTo(b.key.timestamp)); + } else if (_sortPreference == MenuSort.timestampDesc) { + likedOnlyUsers.sort((a, b) => b.key.timestamp.compareTo(a.key.timestamp)); + matchedUsers.sort((a, b) => b.key.timestamp.compareTo(a.key.timestamp)); + } else if (_sortPreference == MenuSort.nameAsc) { + likedOnlyUsers.sort((a, b) => _compareNames(a, b)); + matchedUsers.sort((a, b) => _compareNames(a, b)); } 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)); + likedOnlyUsers.sort((a, b) => _compareNames(b, a)); + matchedUsers.sort((a, b) => _compareNames(b, a)); } - if (_orderPreference == ViewOrder.swipedFirst) { - return [...likedOnlyUsers, ...matchedUsers]; - } else { + if (_orderPreference == ViewOrder.matchedFirst) { return [...matchedUsers, ...likedOnlyUsers]; + } else if (_orderPreference == ViewOrder.swipedOnly) { + return [...likedOnlyUsers]; + } else if (_orderPreference == ViewOrder.matchedOnly) { + return [...matchedUsers]; + } else { + // default: swipesFirst + return [...likedOnlyUsers, ...matchedUsers]; } } + int _compareNames(MapEntry a, + MapEntry b) { + final dataA = (a.value.data() as Map); + final dataB = (b.value.data() as Map); + + final nameA = + dataA.isNotEmpty && dataA.containsKey(Constants.dbFieldUsersName) + ? a.value[Constants.dbFieldUsersName] + : ''; + final nameB = + dataB.isNotEmpty && dataB.containsKey(Constants.dbFieldUsersName) + ? b.value[Constants.dbFieldUsersName] + : ''; + + return nameA.compareTo(nameB); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -192,11 +245,21 @@ class LikedUsersPageState extends State { value: _orderPreference, items: const [ DropdownMenuItem( - value: ViewOrder.swipedFirst, - child: Text('Swiped First')), + value: ViewOrder.swipedFirst, + child: Text('Swipes First'), + ), DropdownMenuItem( - value: ViewOrder.matchedFirst, - child: Text('Matched First')), + value: ViewOrder.matchedFirst, + child: Text('Matches First'), + ), + DropdownMenuItem( + value: ViewOrder.swipedOnly, + child: Text('Swipes Only'), + ), + DropdownMenuItem( + value: ViewOrder.matchedOnly, + child: Text('Matches Only'), + ), ], onChanged: (value) { // update UI on change only @@ -239,66 +302,82 @@ class LikedUsersPageState extends State { title: const Text('Name Descending'), ), ), + PopupMenuItem( + value: MenuSort.timestampAsc, + child: ListTile( + leading: _sortPreference == MenuSort.timestampAsc + ? const Icon(Icons.check) + : const Icon(null), + title: const Text('Oldest first'), + ), + ), + PopupMenuItem( + value: MenuSort.timestampDesc, + child: ListTile( + leading: _sortPreference == MenuSort.timestampDesc + ? const Icon(Icons.check) + : const Icon(null), + title: const Text('Newest first'), + ), + ), ], ), ), ], ), ), - buildLikedUserList(), + _isLoading + ? const Center(child: CircularProgressIndicator()) + : _fetchError != null + ? Center(child: Text('Error: $_fetchError')) + : _likedUsersWithSwipes.isEmpty + ? const Center(child: Text('No liked users found.')) + : buildLikedUserList(), ], ), ); } Widget buildLikedUserList() { - return Expanded( - child: FutureBuilder>( - future: _fetchSortedLikedUsers(), - builder: (BuildContext context, - AsyncSnapshot> snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Center(child: Text('Error: ${snapshot.error}')); - } else if (!snapshot.hasData || snapshot.data!.isEmpty) { - return const Center(child: Text('No liked users found.')); - } else { - return ListView.builder( - itemCount: snapshot.data!.length, - itemBuilder: (BuildContext context, int index) { - DocumentSnapshot user = snapshot.data![index]; - return FutureBuilder( - future: _hasMatch(user.id), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const CircularProgressIndicator(); - } - bool hasMatch = snapshot.data ?? false; - return UserTileLikes( - user: user, - hasMatch: hasMatch, - onUnlike: () { - Map userMap = - user.data() as Map; - bool hasName = - userMap.containsKey(Constants.dbFieldUsersName); + var sortedSwipeUsers = _getSortedLikedUsersWithSwipes(); - _showConfirmationDialog( - user.id, - (hasName - ? user[Constants.dbFieldUsersName] - : 'Name: n/a'), - ); - }, //_unlikeUser(user.id), - onShowMatchMessage: _showMatchMessage, - onViewInfo: () => _showUserInfo(user), - ); - }, - ); - }, - ); - } + return Expanded( + child: ListView.builder( + itemCount: sortedSwipeUsers.length, + itemBuilder: (BuildContext context, int index) { + var entry = sortedSwipeUsers[index]; + var user = entry.value; + bool hasMatch = _cachedMatches.containsKey(user.id) + ? _cachedMatches[user.id]! + : false; + + return UserTileLikes( + user: user, + hasMatch: hasMatch, + onUnlike: () async { + Map userMap = + user.data() as Map; + bool hasName = userMap.containsKey(Constants.dbFieldUsersName); + + bool? confirm = await _showConfirmationDialog( + user.id, + (hasName ? user[Constants.dbFieldUsersName] : 'Name: n/a'), + ); + + if (confirm == true) { + bool hasMatch = await _hasMatch(user.id); + if (!hasMatch) { + await _unlikeUser(user.id); + } else { + // abort, update _cachedMatches and UI + _cachedMatches[user.id] = hasMatch; + setState(() {}); + } + } + }, //_unlikeUser(user.id), + onShowMatchMessage: _showMatchMessage, + onViewInfo: () => _showUserInfo(user), + ); }, ), );