Enhanced ConversationsPage

master
Rafael 2024-06-03 16:35:33 +02:00
parent 6aa821a913
commit 4bff849b13
9 changed files with 267 additions and 40 deletions

View File

@ -1,13 +1,21 @@
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 text;
final String headerText;
final String? currentUserId;
final String? otherUserId;
final String? profileImageUrl;
final void Function()? onTap;
const UserTile({
super.key,
required this.text,
required this.headerText,
this.currentUserId,
this.otherUserId,
this.profileImageUrl,
required this.onTap,
});
@ -22,26 +30,110 @@ class UserTile extends StatelessWidget {
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 25),
padding: const EdgeInsets.all(20),
padding: const EdgeInsets.all(10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
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: 20),
// user name
Text(text),
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

@ -27,6 +27,12 @@ class Constants {
static const String dbFieldLocationSubLocality = 'subLocality';
static const String dbFieldLocationPostalCode = 'postalCode';
static const String dbFieldMessageText = 'message';
static const String dbFieldMessageReceiverId = 'receiverID';
static const String dbFieldMessageSenderId = 'senderID';
static const String dbFieldMessageSenderEmail = 'senderEmail';
static const String dbFieldMessageTimestamp = 'timestamp';
static const String dbFieldUsersID = 'uid';
static const String dbFieldUsersEmail = 'email';
static const String dbFieldUsersName = 'name';

View File

@ -1,4 +1,5 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import '../constants.dart';
class Message {
final String senderID;
@ -18,11 +19,22 @@ class Message {
// convert to a map
Map<String, dynamic> toMap() {
return {
'senderID': senderID,
'senderEmail': senderEmail,
'receiverID': receiverID,
'message': message,
'timestamp': timestamp,
Constants.dbFieldMessageSenderId: senderID,
Constants.dbFieldMessageSenderEmail: senderEmail,
Constants.dbFieldMessageReceiverId: receiverID,
Constants.dbFieldMessageText: message,
Constants.dbFieldMessageTimestamp: timestamp,
};
}
// create Message from map
factory Message.fromMap(Map<String, dynamic> map) {
return Message(
senderID: map[Constants.dbFieldMessageSenderId],
senderEmail: map[Constants.dbFieldMessageSenderEmail],
receiverID: map[Constants.dbFieldMessageReceiverId],
message: map[Constants.dbFieldMessageText],
timestamp: map[Constants.dbFieldMessageTimestamp],
);
}
}

View File

@ -9,14 +9,13 @@ 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'),
title: const Text('Your Chat Contacts'),
backgroundColor: Colors.transparent,
foregroundColor: Colors.grey.shade800,
elevation: 0,
@ -54,8 +53,8 @@ class ConversationsPage extends StatelessWidget {
}
return ListView(
children: matchedUsers
.map<Widget>(
(userData) => _buildUserListItem(userData, context))
.map<Widget>((userData) =>
_buildUserListItem(currentUser.uid, userData, context))
.toList(),
);
} else {
@ -66,10 +65,12 @@ class ConversationsPage extends StatelessWidget {
}
// build individual user list item
Widget _buildUserListItem(
Map<String, dynamic> userData, BuildContext context) {
Widget _buildUserListItem(String currentUserId, Map<String, dynamic> userData,
BuildContext context) {
return UserTile(
text: userData[Constants.dbFieldUsersEmail],
headerText: userData[Constants.dbFieldUsersName],
currentUserId: currentUserId,
otherUserId: userData[Constants.dbFieldUsersID],
profileImageUrl: userData[Constants.dbFieldUsersProfilePic],
onTap: () {
// tapped on a user -> go to chat page

View File

@ -229,15 +229,13 @@ class EditProfilePageState extends State<EditProfilePage> {
CircleAvatar(
radius: 50,
backgroundImage: _profileImage != null
? FileImage(_profileImage!) as ImageProvider
: _webProfileImage != null
? FileImage(_profileImage!)
: (_webProfileImage != null
? MemoryImage(_webProfileImage!)
as ImageProvider // web specific
: (widget.userData.profilePictureUrl != null &&
widget.userData.profilePictureUrl!.isNotEmpty
? NetworkImage(widget.userData.profilePictureUrl!)
as ImageProvider
: null),
: null as ImageProvider<Object>?)),
child: ClipOval(
child: _profileImage == null &&
_webProfileImage == null &&

View File

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

View File

@ -6,7 +6,6 @@ import '../../utils/helper.dart';
import '../../models/message.dart';
class ChatService {
// get instance of firestore and auth
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final FirebaseAuth _auth = FirebaseAuth.instance;
@ -29,12 +28,67 @@ class ChatService {
// construct chat room ID for the two users
String chatRoomID = getCompoundId([currentUserID, receiverID]);
// create batch to add message and update last message details in one step
WriteBatch batch = _firestore.batch();
// add new message to database
await _firestore
var newMessageRef = _firestore
.collection(Constants.dbCollectionChatRooms)
.doc(chatRoomID)
.collection(Constants.dbCollectionMessages)
.add(newMessage.toMap());
.doc(); // doc generates a random ID for the message
batch.set(newMessageRef, newMessage.toMap());
// update last message details in chat room
var chatRoomRef =
_firestore.collection(Constants.dbCollectionChatRooms).doc(chatRoomID);
// add to batch above
batch.set(
chatRoomRef,
{
Constants.dbFieldMessageSenderId: currentUserID,
Constants.dbFieldMessageSenderEmail: currentUserEmail,
Constants.dbFieldMessageTimestamp: timestamp,
},
SetOptions(merge: true));
// commit batch
await batch.commit();
}
/// Retrieves the last message of a chatroom.
Future<Message> getLastMessage(String chatRoomID) async {
String senderID = '';
String senderEmail = '';
String receiverID = '';
String message = '';
Timestamp timestamp = Timestamp.fromDate(DateTime.utc(1970, 01, 01));
QuerySnapshot messageSnapshot = await _firestore
.collection(Constants.dbCollectionChatRooms)
.doc(chatRoomID)
.collection(Constants.dbCollectionMessages)
.orderBy(Constants.dbFieldMessageTimestamp, descending: true)
.limit(1)
.get();
DocumentSnapshot lastMessageDoc = messageSnapshot.docs.first;
if (lastMessageDoc.exists) {
senderID = lastMessageDoc[Constants.dbFieldMessageSenderId];
senderEmail = lastMessageDoc[Constants.dbFieldMessageSenderEmail];
receiverID = lastMessageDoc[Constants.dbFieldMessageReceiverId];
message = lastMessageDoc[Constants.dbFieldMessageText];
timestamp = lastMessageDoc[Constants.dbFieldMessageTimestamp];
}
return Message(
senderID: senderID,
senderEmail: senderEmail,
receiverID: receiverID,
message: message,
timestamp: timestamp,
);
}
// get messages
@ -45,7 +99,7 @@ class ChatService {
.collection(Constants.dbCollectionChatRooms)
.doc(chatRoomID)
.collection(Constants.dbCollectionMessages)
.orderBy('timestamp', descending: false)
.orderBy(Constants.dbFieldMessageTimestamp, descending: false)
.snapshots();
}
}

View File

@ -1,3 +1,4 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
@ -17,6 +18,49 @@ String getCompoundId(List<String> ids) {
return ids.join('_');
}
/// Returns a date format of '$weekday, $day. $month $year $hours:$minutes'.
/// For example: Sat. 3 Jun. 2024 15:03.
/// If any errors occur, an empty string will be returned.
String formatTimestamp(Timestamp timestamp) {
try {
const List<String> weekdays = [
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat',
'Sun'
];
const List<String> months = [
'Jan.',
'Feb.',
'Mar.',
'Apr.',
'May',
'Jun.',
'Jul.',
'Aug.',
'Sep.',
'Oct.',
'Nov.',
'Dec.'
];
DateTime dateTime = timestamp.toDate();
String weekday = weekdays[dateTime.weekday - 1];
int day = dateTime.day;
String month = months[dateTime.month - 1];
int year = dateTime.year;
int hour = dateTime.hour;
int minute = dateTime.minute;
return '$weekday, $day. $month $year ${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}';
} catch (e) {
return '';
}
}
///
/// Get the [displayName] of our own Enumerations.
///

View File

@ -1,3 +1,4 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:cofounderella/utils/helper.dart';
import 'package:flutter_test/flutter_test.dart';
@ -22,7 +23,7 @@ void main() {
});
test('returns correct compound ID for empty elements', () {
List<String> ids = ['id1','','id2',''];
List<String> ids = ['id1', '', 'id2', ''];
String result = getCompoundId(ids);
expect(result, '__id1_id2');
});
@ -78,4 +79,23 @@ void main() {
expect(result, false);
});
});
group('formatTimestamp', () {
test('formatTimestamp returns the expected formatted date for 01.01.1970',
() {
Timestamp timestamp = Timestamp.fromDate(DateTime(1970, 1, 1, 23, 59));
String expectedFormattedDate = 'Thu, 1. Jan. 1970 23:59';
String formattedDate = formatTimestamp(timestamp);
expect(formattedDate, expectedFormattedDate);
});
test(
'formatTimestamp returns the expected formatted date for another timestamp',
() {
Timestamp timestamp = Timestamp.fromDate(DateTime(2023, 8, 15));
String expectedFormattedDate = 'Tue, 15. Aug. 2023 00:00';
String formattedDate = formatTimestamp(timestamp);
expect(formattedDate, expectedFormattedDate);
});
});
}