Chat Logic Flutter Firestore

Raja Ibrahim
5 min readFeb 26, 2024

--

This article explains how you can create chat logic for your app. Moreover, I will talk in context of flutter and firebase, but you can use this logic for other platforms.

A chat room is a place, where two people or maybe in a group multiple people exchange messages. So, we can create a collection “ChatRooms” containing documents (chat rooms) with a unique identifier in our firestore database.

Lets divide this article into modules for better understanding

  • Create chat room
  • Create a group
  • Retrieve my chat rooms
  • Messaging Scheme

Before creating a chat room between two participants in a one-to-one chat, we should figure out if the chat already exists. If it exists, navigate to that particular chat room, else create one.

How to check if a chat room exists?

By formatting the chat room’s document id in such a way that it contain ids of the two participants, we can easily query documents to check if a chat room exists. One way of formatting chat room id is to combine the user ids of two participants. But there is one confusion, whose user id should I place first and whose user id after?

Suppose there are two users using my chat application. There user ids are:

  • user 1: “abc”
  • user 2 : “xyz”

Lets say user 1 creates a chat room with user 2. The id for this room is simply made by combining the ids e.g. “abcxyz”.

Now, user 2 wants to know if he has a chat room with user 1 or not? He can simply combine the his and other’s user id and run a query e.g. “xyzabc”. Did you notice what went wrong? Yeah the ordering. There should be a mechanism, followed by both users, which produce same combined room id by both users.

Don’t worry, this is something do-able. When combing the ids just compare them, place the id with ASCII sum smaller at first place and the other at second place.

final result = myUid.compareTo(participantUid);
var combinedUid = '';
if (result > 0) {
combinedUid = myUid + participantUid;
} else {
combinedUid = participantUid + myUid;
}

This way both users will produce same chat room id and use it to check if a chat room already exists between them or not. If it dose not exits, you can create one.

Future<void> createOneToOneChatRoom(
String combinedUid,
List<String> participantsUid,
List<String> participantsUsername,
List<String> participantsImageUrl
)async{
await FirebaseFirestore.instance.collection('ChatRooms').doc(combinedUid).
set({
'participants_uid': participantsUid,
'particpants_usernames': participantsUsername,
'particpants_imageUrl': participantsImageUrl,
'group_name': '', // left empty, indicating it is not a group.
});
}

The list of string, participants, contains the users ids of participants of chat room. Make sure to sort the user ids the way you combined user ids. For example if the combined user id of two participants is “abcxyz” the participants array should look like [“abc” , “xyz”] not like [“xyz” , “abc”]. Similarly, sort the participants username and participants image url. Finally your chat room data will look like this:

    /*
User 1
id : 'abc',
username : 'chuno',
imageUrl : chuno.jpeg
User 2
id : 'xyz',
username : 'muno',
imageUrl : muno.jpeg
*/
'participants_uid': ['abc','xyz'],
'particpants_usernames': ['chuno','muno'],
'particpants_imageUrl': ['chuno.jpeg','muno.jpeg'],
// data related to user 1 is on the zeroth index of all arrays.

Why on earth we doing this sorting?

Don’t be sad, there is reason. By placing information of one participant at zeroth index for all lists and other participant’s on first index for all lists, we can easily extract participants information. Lets say, chuno with user id “xyz” request a chat room with chat room id “abcxyz”. After receiving the chat room data, how will chuno know on what index of participantsUsername is my data placed. Since chuno knows his own user id, he can compare his user id with the array participantsUid. After comparing, chuno will find out that his user id is placed at first index, meaning other participant’s information is at zeroth index.

To create a group you don’t need to format the room id. Since same participants can create as many groups as they want. So, we can assign a random chat room id for a group.

Future<void> createGroup(
String groupName,
String groupImageUrl,
List<String> participantsUid,
List<String> participantsUsername,
)async{
await FirebaseFirestore.instance.collection('ChatRooms').doc().set({
'group_name': groupName,
'group_image': groupImageUrl,
'participants_uid': participantsUid,
'particpants_usernames': participantsUsername,
});
}

Also, we don’t need to index participants data in case of group creation. In the next module, I will explain in detail about why we index in one-on-one chat and not in group chat.

Retrieval of chat rooms is fairly simple. You need to run a query saying I want all those chat room whose participants field contains my user id.

Stream<QuerySnapshot<Map<String, dynamic>>> chatrooms()async{
var snapshot = await FirebaseFirestore.instance.collection('ChatRooms')
.where('participants_uid',
arrayContains: FirebaseAuth.instance.currentUser!.uid).snapshots();
return snapshot;
}
Chat rooms muno

The above image shows the chat rooms of muno. The red-outlined container is one-on-one chat while the orange-outlined container is a group chat. So the question we were discussing was

Why we indexed arrays in one-on-one chat, but not in a group chat?

Lets make it simple, consider the chat room of chuno. What information about this room dose front-end have? The information we stored while creating that chat room e.g.

'participants_uid': ['abc','xyz'],
'particpants_usernames': ['chuno','muno'],
'particpants_imageUrl': ['chuno.jpeg','muno.jpeg'],

With above information, how will front-end determine whose name and image to display. When the chat room is displayed on the mobile phone of chuno, the chat room should be represented by the information of other participant e.g. muno. And when chat room is displayed on the mobile phone of muno, the chat room should be represented by the information of the other participant e.g. chuno.

Since a user knows his user id, it can compare it with participantsUid as shown below.

int getParticipantIndex(List<String> participants,String myUid) {
return participants[0] == myUid ? 1 : 0;
}

Now chuno knows the index containing data of his chat buddy muno. Using this index, he can extract muno’s username and image url. He can then display this username and image on screen to represent his chat room with muno.

However, in case of a group, the group name as well as the group image is same for all participants, thus no indexing of participant data is required.

Note: Indexing can have multiple meanings in different contexts. But here I mean that if zeroth index contains user id of first user, then all other arrays will contain information related to first user in their zeroth index.

For each chat room, we need a sub collection named messages. Each message will be represented by a unique id and contain fields such as timestamp, content, seen by list, sender user id etc.

Future<void> sendMessage(
String roomId,
Timestamp timestamp,
String content,
String senderUid,
)async{
await FirebaseFirestore.instance
.collection('ChatRooms')
.doc(roomId)
.collection('Messages')
.doc()
.set({
'timestamp': timestamp,
'message_content': content,
'sender_uid': senderUid,
'seen_by': [senderUid]
});
}

When a participant views a message, seen_by array is updated by inserting the user id of participant.

--

--

Raja Ibrahim

Flutter developer with a knack for blockchain innovation and a love for unraveling the mysteries of physics.