14

Im following Firebase guide to structuring data for a chat app. They suggest the structure as seen below.

{
  // Chats contains only meta info about each conversation
  // stored under the chats's unique ID
  "chats": {
    "one": {
      "title": "Historical Tech Pioneers",
      "lastMessage": "ghopper: Relay malfunction found. Cause: moth.",
      "timestamp": 1459361875666
    },
    "two": { ... },
    "three": { ... }
  },

  // Conversation members are easily accessible
  // and stored by chat conversation ID
  "members": {
    // we'll talk about indices like this below
    "one": {
      "ghopper": true,
      "alovelace": true,
      "eclarke": true
    },
    "two": { ... },
    "three": { ... }
  },

  // Messages are separate from data we may want to iterate quickly
  // but still easily paginated and queried, and organized by chat
  // converation ID
  "messages": {
    "one": {
      "m1": {
        "name": "eclarke",
        "message": "The relay seems to be malfunctioning.",
        "timestamp": 1459361875337
      },
      "m2": { ... },
      "m3": { ... }
    },
    "two": { ... },
    "three": { ... }
  }
}

How do I structure my user data so that I can easily display a list of all of the chats they are part of and for each one of them display the last message and timestamp. If I do the following structure:

   "users": {
    "ghopper": {
      "name": "Gary Hopper",
      "chats": {
          "one: true",
          "two": true
      }
    },
    "alovelace" { ... }
  },

I can easily get a list of each chat group for a specific user, for example ghopper, by doing (in swift):

ref.child("users").child("ghopper").child("chats").observeSingleEventOfType(.Value, withBlock: { (snapshot) in
  //do something with data
}

However I won't have the lastMessage and timestamp in this snapshot. What do I need to do to access this data?

  • Duplicate all this data for each user? i.e adding users/ghopper/chats/one/ {"lastMessage": "ghopper: Relay malfunction found. Cause: moth.", "timestamp" : 1459361875666}
  • Make a query for "chats/specificGroupId" for each chat that the user is part of (adding multiple listners)?
  • Some other way?
Nilsymbol
  • 515
  • 1
  • 9
  • 25

2 Answers2

17

How do I structure my user data so that I can easily display a list of all of the chats they are part of and for each one of them display the last message and timestamp.

Change the chats structure a tad by adding users who are in the chat node

"chats": {
    "one": {
      "title": "Historical Tech Pioneers",
      "lastMessage": "ghopper: Relay malfunction found. Cause: moth.",
      "timestamp": 1459361875666
      users
       uid_1: true
       uid_3: true
    },
    "two": { ... },

Then you can deep query for all chats a particular user is part of - this will return the chats uid_3 is involved in

chatsRef.queryOrderedByChild("users/uid_3").queryEqualToValue(true)
     .observeSingleEventOfType(.Value, withBlock: { snapshot in

     //.Value can return multiple nodes within the snapshot so iterate over them
     for child in snapshot.children {
          let lastmsg = child.value["lastMessage"] as! String
          let timestamp = child.value["timestamp"] as! String
          print(lastmsg)
          print(timestamp)
     }     
})

Note that each firebase user has a discreet user id obtained when the user is created via auth.uid. This should (generally) be used as the key for each user.

Jay
  • 34,438
  • 18
  • 52
  • 81
  • 1
    Hi Jay, thanks for your answer. I've tried your solution and it seems to do the trick. But won't this operation become way too expensive once I have a lot of users, considering I would need to look through all "chats/" in order to find all of the chats which the user is part of? – Nilsymbol Jun 02 '16 at 20:41
  • 1
    @Nilsymbol Glad it worked! It doesn't seem like you are pulling down megabytes of data, just a few hundred nodes with some children. Firebase is blisteringly fast so doing a query like this on thousands of nodes is super quick; it won't even make Firebase breath hard. We've tested it! – Jay Jun 02 '16 at 20:50
  • @Jay I understand the structure for the most part, just don't understand how users are represented in the "chats" children. Is it an array of the users uid? Could you explain the Boolean's association with the uid a little more please? Thanks so much! -Colbey – justColbs Jun 07 '16 at 02:05
  • @Jay Disregard last comment, I got it now. A little experimenting cleared up my confusion. Thanks! – justColbs Jun 07 '16 at 02:54
  • @Jay how does security fit into this? If my user isn't part of the chat and I added myself to that chat - that means I can query it - and that means all chats are readable by all users. If an existing chat member added me - then that chat member must know my user ID - that mean all users are readable by all users. Is there a way around this? How do you update the index? – Itai Hanski Jun 08 '16 at 14:14
  • @ItaiHanski That's a great question and probably should be asked separately. Rules in Firebase add security and can allow or disallow access to specific nodes by users. A lot of it has to do with how you want to secure the data; you could secure it by only allowing users to access chat nodes they are part of for example. In that case nobody could query any nodes - which would make sense since the user only has access to nodes they are part of. Post another question and lets see what solutions the community provides. – Jay Jun 08 '16 at 14:44
  • @Jay Good idea. Here it is: http://stackoverflow.com/questions/37639443/firebase-database-security-consideration-in-an-inverse-index – Itai Hanski Jun 08 '16 at 15:03
  • @Jay Thanks for a great response. I'm integrating something familiar in my app based off what you've said. However I'm trying to use display pictures for each chat like you would see in a typical 1 to 1 messenger. Would it make sense for this to be a part of the chats or would you recommend I query every chat in my list and then fetch an image for each of them? – juicy89 Sep 11 '18 at 19:46
  • @juicy89 It really depends on your use case. For example, here on SO user id's are attached to a questions/answers and when it's displayed that user id is looked up in the /users table to retrieve the actual name (Jay in my case) and the image I use for my account. That means and extra hit to the database to get that info for each question/answer but it also keeps it consistent. You can technically store the image (encoded) in Firebase (if it's small, thumbnail sized) or keep it in storage. It may be a good question to post with your structure, code and what the use case is. – Jay Sep 11 '18 at 21:01
3

In the block where you have a list of all the chats a user is in, can you do:

var dictionary: [String: Long]   
var lastMessage: String 
for chat in listOfChatsUserIsIn
    ref.child("chats").child("\(chat)").child("lastMessage").observeSingleEventOfType(.Value, withBlock: { (snapshot) in
        lastMessage = snapshot
        ref.child("chats").child("\(chat)").child("timestamp").observeSingleEventOfType(.Value, withBlock: { (snapshot) in
            //add timestamp and last message to dictionary
        }
    }

I don't know how correct my syntax is. Still learning firebase. But, I think this is basically your second suggestion. Don't know how else you would get the data. This would be O(2n) though which isn't bad.

[[Update 1]]

I was being lazy with my code. I put lastMessage = snapshot to save it so you could add it to the dictionary in the next block.

As for Firebase being asynchronous. I think this would still work as long as you use either the timestamp or message as the key and the other as the value in the dictionary. It may be populated out of order, but you could always sort it by timestamp later. Although, yes, this probably is not best practice.

Jay, I like your solution. Wouldn't you have to also list uid_2: false?

Unfortunately, it seems like both these database structures grow by n^2 as users -> inf and as chats -> inf.

Sharud
  • 415
  • 3
  • 9
  • What is the function of lastMesssage = snapshot since you are not using that variable? Also, it's good practice to use self.variable name within a block (closure). Also, the ref.child inside the first ref.child is a duplicate so it will read the same data. The biggest issue is that Firebase is asynchronous and that tight for loop is going to run *way* faster than the data can be returned within the observe block so I think we're run into some data integrity issues - probably should let Firebase do it's 'stuff' asynchronously. – Jay Jun 01 '16 at 22:14
  • To answer the question within your answer.... No, uid_2: false is not needed as if it doesn't exist, it could be false by default (depends on its coded). And no, the database doesn't grow exponentially. If you have 10 users and 1 chat, all 10 users are in that chat so the only thing that grows would be the users node within the chat (in my answer) likewise if you have 1000 users and 100 chats, each chat with two users, that's still only 100 chats each having a users node with two users. Even with 1000 users and 1000 chats, thats not a substantial amount of data. – Jay Jun 02 '16 at 16:56