1
votes

In my chat app, I have private chat between the two users. I intend to set the chat document's id using these two user's docId/uid in such a way that it doesn't depend on the order they're combined and I can determine the chat document's docId using the uid of users irrespective of the order of uid.

I know, I can use where clauses to get the chat doc as well. Is there any major flaw with my approach of generating the chat document's docId? Should I let it be generated automatically and use normal where clauses supported by firestore and limit(1) to get the chat?

basically, it seems I'm looking for is to encrypt uid1 in such a way that it returns a number only and then same with uid2 and then add them together to create the ChatId. This way it'll not depend on the order I use to add them and I can get the chatId and maybe convert that number back to a string using Base64 encode. This way, if I know the users participating in the chat, I can generate the same ChatId. Will that work or is there any flaw to it?

3

3 Answers

3
votes

Converting each user ID to a number and then adding them together will likely lead to collisions. As a simple example, think of the many ways you can add up to the number 5: 0+5, 1+4, 2+3.

This answer builds upon @NimnaPerera's answer.

Method 1: <uid>_<uid>

If your app doesn't plan on using large groups, you can make use of the <uid>_<uid> format. To make sure the two user IDs are ordered in the same way, you can sort them first and then combine them together using some delimiter.

A short way to achieve this is to use:

const docId = [uid1, uid2].sort().join("_");

If you wanted to have a three-way group chat, you'd just add the new userID in the array:

const docId = [uid1, uid2, uid3].sort().join("_");

You could also turn this into a method for readability:

function getChatIdForMembers(userIds) {
  return userIds.sort().join("_");
}

Here's an example of it in action:

const uid1 = "apple";
const uid2 = "banana";
const uid3 = "carrot";

[uid1, uid2].sort().join("_"); // returns "apple_banana"
[uid1, uid3].sort().join("_"); // returns "apple_carrot"
[uid2, uid1].sort().join("_"); // returns "apple_banana"
[uid2, uid3].sort().join("_"); // returns "banana_carrot"
[uid3, uid1].sort().join("_"); // returns "apple_carrot"
[uid3, uid2].sort().join("_"); // returns "banana_carrot"

// chats to yourself are permitted
[uid1, uid1].sort().join("_"); // returns "apple_apple"
[uid2, uid2].sort().join("_"); // returns "banana_banana"
[uid3, uid3].sort().join("_"); // returns "carrot_carrot"

// three way chat
[uid1, uid2, uid3].sort().join("_"); // returns "apple_banana_carrot"
[uid1, uid3, uid2].sort().join("_"); // returns "apple_banana_carrot"
[uid2, uid1, uid3].sort().join("_"); // returns "apple_banana_carrot"
[uid2, uid3, uid1].sort().join("_"); // returns "apple_banana_carrot"
[uid3, uid1, uid2].sort().join("_"); // returns "apple_banana_carrot"
[uid3, uid2, uid1].sort().join("_"); // returns "apple_banana_carrot"

Method 2: Member list properties

If you intend on supporting group chats, you should use automatic document IDs (see CollectionReference#add()) and store a list of chat members as one of it's fields as introduced in @NimnaPerera's answer for better use of queries.

I recommend two fields:

  • "members" - an array containing each chat member's ID. This allows you to query the /chats collection for chats that contain the given user.
  • "membersAsString" - a string, built from sorting "members" and joining them using "_". This allows you to query the /chats collection for chats that contain the exact list of members.
"chats/{chatId}": {
  "members": string[], // list of users in this chat
  "membersAsString": string, // sorted list of users in this chat, delimited using "_"
  /* ... */
}

To find all chats that I am a part of:

const myUserId = firebase.auth().currentUser.uid;

const myChatsQuery = firebase.firestore()
  .collection("chats")
  .where("members", "array-contains", myUserId);

myChatsQuery.onSnapshot(querySnapshot => {
  // do something with list of chat documents
});

To find all three-way chats between Apple, Banana and I:

const myUserId = firebase.auth().currentUser.uid;
const members = [myUserId, "banana", "apple"];
const membersAsString = members.sort().join("_");

const groupChatsQuery = firebase.firestore()
  .collection("chats")
  .where("membersAsString", "==", membersAsString);

groupChatsQuery.onSnapshot(querySnapshot => {
  // do something with list of chat documents
  // normally this would return 1 result, but you may get
  // more than one result if a user gets added/removed a chat
});

A normal flow, would be to:

  1. Get a list of the relevant chats
  2. For each chat, get the most recent message
  3. Based on the most recent message, sort the chats in your UI
1
votes

You can very well use a combination of two users uids to define your Firestore document IDs, as soon as you respect the following constraints:

  • Must be valid UTF-8 characters
  • Must be no longer than 1,500 bytes
  • Cannot contain a forward slash (/)
  • Cannot solely consist of a single period (.) or double periods (..)
  • Cannot match the regular expression __.*__

What I'm not sure to understand in your question is "in such a way that it doesn't depend on the order they're combined". If you combine the uids of two users you need to combine them in a certain order. For example, uid1_uid2 is not equal to ui2_uid1.

1
votes

As you are asking @lightsaber you can follow following methods to achieve your objective. But my personal preference is using an where clause, because firestore is supporting that compound queries which cannot be done in real time database.

Method 1

Create a support function to generate a chatId and check whether document is exist from that id. Then you can create chat document or retrieve the document using that id.

const getChatId = (currentUserId: string, guestUserId: string) => {
    /* In this function whether you changed the order of the values when passing as parameters
    it will always return only one id using localeCompare */
    
    const comp = currentUserId.localeCompare(guestUserId);
    if (comp === 0) {
        return null;
    }

    if (comp === -1) {
        return currentUserId + '_' + guestUserId;
    } else {
        return guestUserId + '_' + currentUserId;
    }
}

Method 2

Use where clause with array-contains query for retrieving the chat document. And when creating add two user Ids to array and set the array with a relevant field name.

Firestore docs for querying arrays