LikedUsersPage: Sort View

master
Rafael 2024-06-04 20:12:12 +02:00
parent fec48737c7
commit cddb2078ed
7 changed files with 369 additions and 200 deletions

View File

@ -116,29 +116,10 @@ class MyDrawer extends StatelessWidget {
), ),
), ),
// settings list tile
Padding( Padding(
padding: const EdgeInsets.only(left: 25), padding: const EdgeInsets.only(left: 25),
child: ListTile( child: ListTile(
title: const Text("My Profile"), 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'),
leading: const Icon(Icons.edit_note), leading: const Icon(Icons.edit_note),
onTap: () { onTap: () {
// pop the drawer first, then navigate to destination // 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 // TODO TESTING - user data tile
Padding( Padding(
padding: const EdgeInsets.only(left: 25), padding: const EdgeInsets.only(left: 25),

View File

@ -1,21 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../models/message.dart';
import '../services/chat/chat_service.dart';
import '../utils/helper.dart';
class UserTile extends StatelessWidget { class UserTile extends StatelessWidget {
final String headerText; final String text;
final String? currentUserId;
final String? otherUserId;
final String? profileImageUrl; final String? profileImageUrl;
final void Function()? onTap; final void Function()? onTap;
const UserTile({ const UserTile({
super.key, super.key,
required this.headerText, required this.text,
this.currentUserId,
this.otherUserId,
this.profileImageUrl, this.profileImageUrl,
required this.onTap, required this.onTap,
}); });
@ -30,110 +22,26 @@ class UserTile extends StatelessWidget {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 25), margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 25),
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(20),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
buildProfileIcon(), // Profile image
buildMsgContent(), 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<Message?>(
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,
),
),
],
),
],
),
);
}
},
);
}
} }

View File

@ -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<Message?>(
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,
),
),
],
),
],
),
);
}
},
);
}
}

View File

@ -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<String, dynamic> userMap = user.data() as Map<String, dynamic>;
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,
),
],
),
),
);
}
}

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../components/user_tile.dart'; import '../components/user_tile_chats.dart';
import '../constants.dart'; import '../constants.dart';
import '../services/auth/auth_service.dart'; import '../services/auth/auth_service.dart';
import '../services/user_service.dart'; import '../services/user_service.dart';
@ -67,7 +67,7 @@ class ConversationsPage extends StatelessWidget {
// build individual user list item // build individual user list item
Widget _buildUserListItem(String currentUserId, Map<String, dynamic> userData, Widget _buildUserListItem(String currentUserId, Map<String, dynamic> userData,
BuildContext context) { BuildContext context) {
return UserTile( return UserTileChats(
headerText: userData[Constants.dbFieldUsersName], headerText: userData[Constants.dbFieldUsersName],
currentUserId: currentUserId, currentUserId: currentUserId,
otherUserId: userData[Constants.dbFieldUsersID], otherUserId: userData[Constants.dbFieldUsersID],

View File

@ -59,7 +59,7 @@ class HomePage extends StatelessWidget {
if (userData[Constants.dbFieldUsersEmail] != if (userData[Constants.dbFieldUsersEmail] !=
_authService.getCurrentUser()!.email) { _authService.getCurrentUser()!.email) {
return UserTile( return UserTile(
headerText: userData[Constants.dbFieldUsersEmail], text: userData[Constants.dbFieldUsersEmail],
onTap: () { onTap: () {
// tapped on a user -> go to chat page // tapped on a user -> go to chat page
Navigator.push( Navigator.push(

View File

@ -1,10 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import '../components/user_tile_likes.dart';
import '../constants.dart'; import '../constants.dart';
import '../services/auth/auth_service.dart'; import '../services/auth/auth_service.dart';
import '../utils/helper.dart'; import '../utils/helper.dart';
enum MenuSort { nameAsc, nameDesc, timestampAsc, timestampDesc }
enum ViewOrder { swipedFirst, matchedFirst }
class LikedUsersPage extends StatefulWidget { class LikedUsersPage extends StatefulWidget {
const LikedUsersPage({super.key}); const LikedUsersPage({super.key});
@ -15,6 +20,9 @@ class LikedUsersPage extends StatefulWidget {
class LikedUsersPageState extends State<LikedUsersPage> { class LikedUsersPageState extends State<LikedUsersPage> {
final String currentUserId = AuthService().getCurrentUser()!.uid; final String currentUserId = AuthService().getCurrentUser()!.uid;
ViewOrder _orderPreference = ViewOrder.swipedFirst;
MenuSort _sortPreference = MenuSort.nameAsc;
Future<List<DocumentSnapshot>> _fetchLikedUsers() async { Future<List<DocumentSnapshot>> _fetchLikedUsers() async {
QuerySnapshot likedUsersSnapshot = await FirebaseFirestore.instance QuerySnapshot likedUsersSnapshot = await FirebaseFirestore.instance
.collection(Constants.dbCollectionUsers) .collection(Constants.dbCollectionUsers)
@ -134,14 +142,119 @@ class LikedUsersPageState extends State<LikedUsersPage> {
); );
} }
Future<List<DocumentSnapshot>> _fetchSortedLikedUsers() async {
List<DocumentSnapshot> likedUsers = await _fetchLikedUsers();
List<DocumentSnapshot> likedOnlyUsers = [];
List<DocumentSnapshot> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Liked Users'), title: const Text('Liked Users'),
), ),
body: FutureBuilder<List<DocumentSnapshot>>( body: Column(
future: _fetchLikedUsers(), children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
DropdownButton<ViewOrder>(
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<MenuSort>(
icon: const Icon(Icons.sort),
onSelected: (MenuSort item) {
// update UI on change only
if (_sortPreference != item) {
setState(() {
_sortPreference = item;
});
}
},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<MenuSort>>[
PopupMenuItem<MenuSort>(
value: MenuSort.nameAsc,
child: ListTile(
leading: _sortPreference == MenuSort.nameAsc
? const Icon(Icons.check)
: const Icon(null),
title: const Text('Name Ascending'),
),
),
PopupMenuItem<MenuSort>(
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<List<DocumentSnapshot>>(
future: _fetchSortedLikedUsers(),
builder: (BuildContext context, builder: (BuildContext context,
AsyncSnapshot<List<DocumentSnapshot>> snapshot) { AsyncSnapshot<List<DocumentSnapshot>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
@ -162,7 +275,7 @@ class LikedUsersPageState extends State<LikedUsersPage> {
return const CircularProgressIndicator(); return const CircularProgressIndicator();
} }
bool hasMatch = snapshot.data ?? false; bool hasMatch = snapshot.data ?? false;
return UserInfoTile( return UserTileLikes(
user: user, user: user,
hasMatch: hasMatch, hasMatch: hasMatch,
onUnlike: () { onUnlike: () {
@ -191,68 +304,3 @@ class LikedUsersPageState extends State<LikedUsersPage> {
); );
} }
} }
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<String, dynamic> userMap = user.data() as Map<String, dynamic>;
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,
),
],
),
),
);
}
}