Enhanced ConversationsPage
parent
6aa821a913
commit
4bff849b13
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue