Skip to main content

Threaded discussion models are a simple idea and have been common in Internet discussion platforms since long before the Web. While nested replies make a lot of logical sense, such a model isn't suitable everywhere and you will find plenty of strong opinions about their usability. That aside, actually implementing this sort of reply tree can be quite difficult, not in the least because there are many different ways to do it. Here, we have built a threaded discussion system in MongoDB working with a Node.js content management system based on arrays of ancestors.

Storing Comments

Here is a typical comment stored with this structure. Each comment can be stored, standalone, in a 'comments' collection - we won't need to nest replies all in one document.

{
  _id: 55ce72f,
  content: "I'm a comment!",
  parents: ['55a5947', '55ce72f'],
  author: "Someone"
}

As you can see, it has a unique ID (I find generating MongoDB ObjectIDs to be a useful way of assigning such ID numbers, as they are unique and also contain a timestamp), some content attached to it and an array of parents, including itself as the last element.

This means that any one comment in a chain of replies contains the full path back to the root comment. As comments nest deeper, they will have a larger array of parents. For instance, take this chain of replies.

[id: 10] Root comment
|
|-- [id: 11] Reply to root
|   |
|   |-- [id: 12] Reply to reply to root
|       |
|       |-- [id: 13] Bottom comment in this chain
|
|-- [id: 19] Another reply to root

The "bottom comment in this chain" will have its parents array looking like this:

{
  ...
  parents: [10, 11, 12, 13]
  ...
}

And the "Another reply to root," like this:

{
  ...
  parents: [10, 19]
  ...
}

So, when a comments is added as a response to another comment, assemble it in the CMS as you would normally...

  commentId = new ObjectId();
  
  ...
  
  comment = {
    _id: commentId,
    content: "Indeed, that is a comment"
    ...
  };

And then run a query for that parent comment like in this pseudocode example:

parentComment = db.comments.find(
  {
    _id: parentId
  }
);

Simply grab the parents array from that comment and push in the ID for this new comment at the end - and you have the full path to the root including this comment.

parentComment.parents.push(commentId);
comment.parents = parentComment.parents;

If posting a comment that isn't a reply - one that is on the root level - you need only put the current comment ID into the parents array.

comment.parents  = [commentId]

Fetching the Full Tree

Now, here comes the tricky part. When rendering the comments structure, we want to display everything in this hierarchy that is only defined from the bottom up, rather than from the root downwards. So how can we merge all these threads?

First, query for all the relevant messages, e.g. posts in a group or comments on an article. It doesn't matter how you're doing this, but you'll want to end up with an array of root messages. Then, for each root message:

  • We sort the replies by the length of the parent array, so that you have an array of replies, beginning with the replies that have the most parents and ending with the root message. You might use this classic bit of JavaScript:

        var sort = function (a, b) {
          if (a.parents.length < b.parents.length) {
            return 1;
          }
          if (a.parents.length > b.parents.length) {
            return -1;
          }
          // a must be equal to b
          return 0;
        };
        
        thread.sort(sort);
      
  • Then, we loop through all these replies and push actual messages into a 'replies' object. Like this:

        thread.forEach(function (comment) {
    
          // Ignore root
          if (comment.parents.length > 1) {
    
            // Get the parent comment based on the next-to-last
            // comment ID in this comment's parents
            var parentComment = messagesInThread[comment.parents[comment.parents.length - 2]];
    
            if(parentComment) {
    
              // If the replies array doesn't yet exist, create it
              if (!parentComment.replies) {
                parentComment.replies = [];
              }
    
              parentComment.replies.push(comment);
    
            } else {
    
              // If a parent message can't be found and we haven't yet
              // reached the root comment, we're missing part of the chain
              console.log("Broken reply chain.");
    
            }
    
          }
    
        });
        
        return (thread[thread.length - 1]);
      

    Note the use of length - 2 to find the immediate parent of a comment.

After that, you'll have the root message with all the replies nested in it, for you to render recursively.

Want to work with us?

Want to start something?

Fantastic! We'd love to hear from you, whethere it's a pitch, an idea or just saying hello!

Feel free to get in touch.

 

Drop us an email?

info@citywebconsultants.co.uk

 

Or give us a call?

+44 (0) 191 906 7746

 

Playing hard to get? We'll contact you