How to group chat messages per user?
Asked Answered
G

4

10

I have a group chat message build using Vue.js. I am currently fetching the messages which returns an array like this:

"data":[
    {
        "id":1,
        "message":"<p>yo<\/p>",
        "removed":"false",
        "user":{
            "uid":2,
            "metadata":{
                "username":"Testing"
            }
        },
        "post_date":"2018-02-24 14:30"
    },
    {
        "id":2,
        "message":"<p>test<\/p>",
        "removed":"false",
        "user":{
            "uid":1,
            "metadata":{
                "username":"Admin"
            }
        },
        "post_date":"2018-02-24 22:31"
    },
    {
        "id":3,
        "message":"<p>Wassup?<\/p>",
        "removed":"false",
        "user":{
            "uid":1,
            "metadata":{
                "username":"Admin"
            }
        },
        "post_date":"2018-02-24 22:40"
    },
    {
        "id":12,
        "message":"again for testing post date",
        "removed":"false",
        "user":{
            "uid":1,
            "metadata":{
                "username":"Admin"
            }
        },
        "post_date":"2018-03-04 00:59"
    },
    {
        "id":13,
        "message":"Hello!",
        "removed":"false",
        "user":{
            "uid":2,
            "metadata":{
                "username":"Testing"
            }
        },
        "post_date":"2018-03-04 11:13"
    },
    {
        "id":13,
        "message":"<p>Hi!</p>",
        "removed":"false",
        "user":{
            "uid":2,
            "metadata":{
                "username":"Testing"
            }
        },
        "post_date":"2018-03-04 11:13"
    },
],

At the moment I am just looping through the data and outputting each message into a separate div. What I would prefer is to group the messages when the same user has posted more than once in a row.

How would I go about this? Should it be an option server side (maybe an optional group parameter?) or somehow done client side?

EDIT This is how the chat current looks: How the chat currently looks

And this is the desired look: Desired chat look

The problem if I group them by the UID/Username, is that the messages need to be output in order. So if User 1 send three messages, then User 2 send two, then User 1 sends another message, all of user 1s message will be grouped together. Rather the User 1s three messages should be grouped, then User 2s, then it should show User 1s last message.

Goyette answered 4/3, 2018 at 11:43 Comment(1)
Can you give an example of what you want the output to look like? – Firebreak
S
4

I solved this problem for myself pretty recently. Here's a full example.

The core business logic for grouping messages, in the above example, can be found under src/store.js in the addMessage function.

Unless your server is also storing all the messages and you have some mechanism to ensure causal ordering across all clients, I would recommend that you run the logic on each client. That way, you ensure that clients don't see messages jumping around under any circumstance. At worst, messages would appear ungrouped.

The algorithm to determine this is run when a new message is received, therefore it runs on each client, but you can tweak it to work for your use-case as well! It's shown below (I may have used the wrong flowchart elements..apologies).

addMessage({ state }, { msg, selfHash }) {
  let addAsNewMsg = true;
  const lastMessage = state.messages.slice(-1)[0];
  if (lastMessage && lastMessage.userHash === msg.userHash) {
    // The last message was also sent by the same user
    // Check if it arrived within the grouping threshold duration (60s)
    const lastMessageTime = moment(lastMessage.time);
    const msgTime = moment(msg.time);
    const diffSeconds = msgTime.diff(lastMessageTime, "seconds");
    if (diffSeconds <= 60) {
      addAsNewMsg = false; // We're going to be appending.. so
      lastMessage.message.push(msg.message);
      // We're going to now update the timestamp and (any) other fields.
      delete msg.message; // Since, we've already added this above
      Object.assign(lastMessage, msg); // Update with any remaining properties
    }
  }
  if (addAsNewMsg) {
    state.messages.push(msg);
  }
}
Sprightly answered 29/1, 2019 at 19:41 Comment(2)
What about retrieving older messages? How would they be grouped on retrieval from backend/api? – Metallo
Not a problem. As far as this logic is concerned, an old message is no different from a newly sent chat message. The only thing that matters is the grouping threshold. Do you see some specific issue with this logic? πŸ€” – Sprightly
B
2

Seems like an ideal use case for computed properties. First sort the data by post_date

computed: {
    sortedPosts() {
        return this.posts.sort((a, b) => {
            if (a.post_date < b.post_date) return -1;
            else if (a.post_date > b.post_date) return +1;
            return 0;
        });
    },

Then group by user.uid

    groupedPosts() {
        const posts = this.sortedPosts.reduce((post, grouped) => {
            if (grouped.length === 0) 
                grouped.unshift([post]);
            else if (grouped[0][0].user.uid === post.user.uid)
                grouped[0].push(post);
            else
                grouped.unshift([post]);
            return grouped;
        }, []);
        return posts.reverse();
    }

Then used the computed property in a template

<div v-for="group in groupedPosts">
    <div v-for="post in group">

(I haven't tested the code above, so watch for typos, etc.)

Bobbitt answered 29/1, 2019 at 16:52 Comment(3)
I'm using lodash, Can not I just order it that way? _.orderBy(collection, ['post_date'], ['asc']) – Bowling
I have never used lodash myself, but that seems plausible. Might also be able to use groupBy in some clever way. – Bobbitt
I recently tested your code. The order of params in the reduce callback is swapped. But other than that, it is fine. – Scutate
S
1

My approach would be to keep a reference to the div containing the last user's speech, and a variable representing the uid of the last user to speak. When a message comes in that matches the last uid, you'll simply add it to the div you're keeping track of; if the new uid doesn't match the previous one, you'll simply create a new div, add it to the DOM, and update the uid variable.

// initialize variables
let lastUid,curDiv;

getData().forEach(msg => {
  if (msg.user.uid !== lastUid) {
    // new user is speaking!
    curDiv = document.createElement("div");
    document.body.appendChild(curDiv);
    lastUid = msg.user.uid;
  }
  // add current message to the current div
  curDiv.innerHTML += msg.message;
});

function getData() {
  // really only put this in a function so that I could hoist it so that my logic can go above.
  return [
      {
          "id":1,
          "message":"<p>yo<\/p>",
          "removed":"false",
          "user":{
              "uid":2,
              "metadata":{
                  "username":"Testing"
              }
          },
          "post_date":"2018-02-24 14:30"
      },
      {
          "id":2,
          "message":"<p>test<\/p>",
          "removed":"false",
          "user":{
              "uid":1,
              "metadata":{
                  "username":"Admin"
              }
          },
          "post_date":"2018-02-24 22:31"
      },
      {
          "id":3,
          "message":"<p>Wassup?<\/p>",
          "removed":"false",
          "user":{
              "uid":1,
              "metadata":{
                  "username":"Admin"
              }
          },
          "post_date":"2018-02-24 22:40"
      },
      {
          "id":12,
          "message":"again for testing post date",
          "removed":"false",
          "user":{
              "uid":1,
              "metadata":{
                  "username":"Admin"
              }
          },
          "post_date":"2018-03-04 00:59"
      },
      {
          "id":13,
          "message":"Hello!",
          "removed":"false",
          "user":{
              "uid":2,
              "metadata":{
                  "username":"Testing"
              }
          },
          "post_date":"2018-03-04 11:13"
      },
      {
          "id":13,
          "message":"<p>Hi!</p>",
          "removed":"false",
          "user":{
              "uid":2,
              "metadata":{
                  "username":"Testing"
              }
          },
          "post_date":"2018-03-04 11:13"
      },
  ]
}
div {
  border: 1px solid black;
  margin-bottom: 5px;
}
Shantay answered 29/1, 2019 at 16:26 Comment(0)
S
0

You could group your messages by user on the server-side.

Iterate over the data, pushing each object into an object of arrays, where the keys are the uid's and the values are arrays of objects for the respective user.

let dataByUser = {};

//Iterate over your data
for (let i = 0; i < data.length; i++) {
    //If the object doesn't have a key equal to the current objects username, 
    //add it as a new key and set the values to a new array containing this datum.
    if(!dataByUser.hasOwnProperty(data[i].user.uid) {
        dataByUser[data[i].user.uid] = new Array(data[i]);
    } else {
        //If it does exist, push to the array for the existing key.
        dataByUser[data[i].user.uid].push(data[i]);
    }
}

You should end up with an object with a K:V pair per user in your data array, where their values are arrays of objects with all the respective messages in.

You'd then have to write some client code that iterates over this new data structure and formats it however you wish.

Solitude answered 4/3, 2018 at 11:55 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.