The Fluther Blog

Back to Fluther

What is Fluther?

Fluther is a free Q&A collective that specializes in getting
fast answers from the right people. Check it out!

“How’d they do that?” Real-time chat

8:45 am

In Erik’s last entry in the series, robmandu asked how we implemented the real-time chat. So, ask and ye shall receive! There are a lot of moving parts in the chat system, so I’m going to focus mostly on the back-end for this article — which allows me to show you one of my favorite parts of Django: template tags! Yeah!

Before we get started, let’s have a quick run-down of how the chat works when you visit a question on the site:

  • The initial page is rendered with all the responses so-far, including anyone who might be composing currently.
  • Every few seconds, you poll us to see if anyone has started writing a response, or if there are any new responses from people who have finished composing.

Fairly simple! Let’s look at how we accomplish this… but first, a little history:

Before, when we had just started the iPhone version of the site, we built each response up from scratch using DOM utilities in Javascript. That is: when your browser polled the site, you’d get back a list of usernames (and other data) from the server, then we’d hand-build each HTML element of a response and hook in the JavaScript to make the AJAXified “Great Answer” links work correctly.

Since iphone.fluther.com uses a slightly different version of displaying responses, when started making changes to this area of the code we realized that it was a nightmare to maintain. Every time we’d make a change we’d have to alter the code in four places: Twice for when the responses were rendered initially (one for each version), and twice when they were constructed in JavaScript.

Then we decided to switch to AHAH. Instead of receiving a flat list of data from the server, we now send out the fully-formed HTML which the client then places at the end of the list. We had to fudge a little on our 100% compliance with web-standards, but we scored a win on speed and most importantly, maintainability.

So, here’s how it works:

This is how we initially render our responses:

<div id="quiplist">
    {% for quip in quiplist %}
         {% render_quip quip %}
    {% endfor %}
</div>

<div id="composelist">
    {% render_composers composers %}
</div>

As you can see, not much code. Django loops through all the responses in the question and calls the render_quip template tag. Then, it renders all the composers with another template tag. Let’s see what’s in those tags:

First, render_composers.html:

{% for compose in composers %}
<div class="compose">
  <img class="avatar" src="{{compose.imageurl}}" alt="{{compose.username}}'s avatar"/>
  <p>{{compose.username}} is crafting a response&0133;</p>
  <div class="qbar">
    <span class="qspan end">
      <a href="/users/{{compose.username}}/">{{compose.username}}</a> (<span class="score">{{compose.score}}</span>
      <img width="8" height="8" class="trans-png star" alt=" points" src="/static/images/v2/star.png"/>)
    </span>
  </div>
</div>
{% endfor %}

This django template snippet takes a list of user and generates the “Crafting a response” entries.

There’s one aspect of the template tag I’m glossing over: when you call {% render_composers %}, it actually calls a python method “render_composers” which can perform any calculations you need and then pass the variables on to the template I just showed you.

Here’s the method for render_quip:

@register.inclusion_tag('v2/render_quip.html', takes_context=True)
@inherit_panda_vars
def render_quip(context, quip, ahah_format=False):

    user = context.get('user')

    marked_great = False

    if render_actions and user.is_authenticated():
        if user == quip.user:
            marked_great = True
        elif quip.quipvote_set.filter(user=user).count() > 0:
            marked_great = True

    return { "quip": quip,
             "user": user,
             "disc": context.get('disc'),
             "ahah_format": ahah_format,
             "marked_great": marked_great,
            }

Here we calculate a few things… whether we’re in AHAH mode or not (which affects how we calculate the striping of the responses), whether the user has marked this as a Great Answer! or not, etc.

Now, render_quip.html:

{% if not ahah_format %}<div id="quip{{quip.id}}" class="quip {% cycle quip1,quip2 %}
  {% avatar_img quip.user %}
    <div class="message">
      {{quip.message}}
    </div>
    <div class="qbar">

      <span class="qspan">
        <a href="{{quip.user.get_absolute_url}}">{{quip.user.username}}</a>
        (<span class="score">{{quip.user.get_profile.score}}</span>
        <img class="trans-png star" width="8" height="8" src="{{MEDIA_URL}}images/v2/star.png" alt=" points"/>)</span>
	<span class="qspan great-answer">
          {% if marked_great %}
             Great Answer!
          {% else %}
	     <a href="/usefulquip/?id={{quip.id}}">&#8220;Great Answer&#8221;</a>
          {% endif %}
        </span>
       .
       .
       .
      </span>
  </div>
{% if not ahah_format %}</div>{% endif %}

Here we see the striping I talked about (that’s what {% cycle %} does: each alternating response we render gets a different css class). We also see how the marked_great variable that we set in our python method affects the way we display our response.

So how does this all plug into the web browser?

handle_listen: function(transport) {
    var response = eval('(' + transport.responseText + ')');
    var quips = response.quips;

    if (quips.length > 0) {
        fluther_disc.since = response.lastid;

        // zebra striping
        var quiplist = YAHOO.util.Dom.getElementsByClassName("quip", "div", "quiplist"); //select only the divs
        if (quiplist.length > 0 && quiplist[quiplist.length-1].className == "quip quip1"){
             var zebra_classes = ["quip quip2", "quip quip1"];
        } else {
             var zebra_classes = ["quip quip1", "quip quip2"];
        }

        //do the building
        for (var i = 0; i < quips.length; i++){

            new Quip(quips[i].html, quips[i].id, zebra_classes[i%2]);
        }
    }

    //build the compose template
    $('composelist').innerHTML = response.compose_html;


    return response;
}

When we get into the JavaScript, we have to calculate the striping for each new response that gets added, and we just add in the HTML we got back from the server into the webpage. Now, if we need to change the format of the responses (like, say, for the iPhone) we can re-use most of our code!

Coming up next for me: Why we chose YUI over Scriptaculous and MochiKit!