"Who's online" widget in Discourse - my version


Live demo: http://forum.kozovod.com/

Disclosure

This is my take on showing a simple “who’s online” list in my Discourse setup.

NB. This solution does not follow best practices to add functionality to Discourse!
However, I wanted it a lot and had very little time, so I used the tools I knew and had at my hands.

Drawbacks of this solution

  • It uses React.js instead of Ember. Simply because I don’t know Ember, don’t like it and failed learning it in a short period of time (it was the opposite with React).
  • It uses WordPress with Twig Anything plugin - the only reason being that presence information is not available
  • It does not use Discourse message bus. It makes an HTTP request every X seconds or minutes as by your configuration. If you have plenty plenty of users online, this might be bad for your service traffic channel.

Good side

  • It works!
  • It is easily customizable!
  • It is what my Discourse community needs - here and now!
  • It works fine on mobiles as well - tested.

Step 1. Customization section in Discourse

Create a new customization and name it e.g. “React.js”:

</head> section:

<script src="https://fb.me/react-with-addons-0.13.3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.6.15/browser.min.js"></script>

First line loads react, second line loads in-browser compiler for React components. Both are CDN-hosted.

CSS section

For now, use this. Feel free to customize when you got it working:

.who-is-online-user {
    display: inline-block;
    padding: 0 3px;
}
.who-is-online-status {
    display: inline-block;
    margin-right: 2px;
    font-size: 28px;
    vertical-align: middle;
}
.who-is-online-status-online {
    color: green;
}
.who-is-online-status-idle {
    color: orange;
}
.who-is-online-prefix-text {
    color: white;
    background-color: gray;
    display: inline-block;
    margin-right: 4px;
    padding: 1px 4px;
}

Copy the same CSS code to the mobile CSS section.

</body> section

<section id="who-is-online-widget"></section>

<script type="text/babel">

var WhoIsOnlineWidget = React.createClass({
    getInitialState: function() {
        return {
            updating: false,
            data: []
        };
    },
    updateUsersList: function() {
        if (this.state.updating) {
            return;
        }
        var me = this;
        this.setState({updating: true}, function() {
            jQuery.ajax(this.props.apiUrl, {
                method: "POST"
            })
            .done(function(data) {
                // On success, update the list
                if (!data || !data.code || data.code !== 'OK' || !data.result) {
                    return;
                }
                me.setState({data: data.result});
            })
            .fail(function() {
                // On failure do nothing
            })
            .always(function(){
                // Always plan the next fetch
                me.setState({updating: false}, function() {
                    setTimeout(me.updateUsersList, me.props.updateSeconds);
                });
            });
        });
    },
    render: function() {
        var items = [];
        for (var i = 0; i < this.state.data.length; i++) {
            var user = this.state.data[i];
            items.push(
                <div className="who-is-online-user">
                    <span className={"who-is-online-status who-is-online-status-"+user.status}>{"\u2022"}</span>
                    <a className="userNickname"
                        href={user.user_absolute_url}>
                        {user.user_from_api.username}
                    </a>
                </div>
            );
        }
        var prefixText = null;
        if (this.props.prefixText.length > 0) {
            prefixText = (
                <span className="who-is-online-prefix-text">
                    {this.props.prefixText}
                </span>
            );
        }
        return (
            <div className="container">
                {prefixText}
                {items}
            </div>
        );
    }
});

React.render(
    <WhoIsOnlineWidget
        apiUrl="http://kozovod.com/api-endpoint/whos-online/"
        updateSeconds={300000}
        prefixText="Who's online"/>,
    document.getElementById('who-is-online-widget'),
    function(){
        this.updateUsersList();
    }
);

</script>

apiUrl=“http://kozovod.com/api-endpoint/whos-online/ - URL to the API that we will build in WordPress, details follow.

updateSeconds={300000} - how often to update the online status in user browser, in milliseconds. I set it to 5 minutes to avoid any trouble with my very little WordPress server instance.

prefixText=“Who’s online” - the text you would like to be shown in a gray box.


Step 2. New API Endpoint in WordPress

Yes, it sounds really weird, but it works:

  1. I’m using WordPress to fetch info from Discourse with using my admin api key
  2. In WordPress, I’m using Twig Anything plugin and its API Endpoints add-on to build my own API in WordPress.
  3. From user browser who’s exploring Discourse, I query my WordPress API to get the info I need.

Why I did so:

  1. To keep Discourse admin API hidden from public. I could request the API directly from user browser, but it would reveal API key, which is no-no.
  2. It saved me time to use the tools I already have in place, i.e. the Twig Anything plugin and its add-ons.

Data Source configuration

Create a Twig Template in your WordPress and configure its data source section as on the following screenshot:

The URL is:

http://your-discourse.com/admin/users/list/active.json?api_username=XXX&api_key=YYY

XXX your Discourse admin username
YYY your Discourse api key


Twig Template

Here is the template code. Note that it outputs a JSON-array, which we are going to make a part of API responst with using the API Endpoints add-on:

[
{% set num = 1 %}
{% for user in data if (not user.suspended and not user.blocked and user.username != 'api_ru') %}
  {% if num <= 11 %}

    {% if not loop.first %},{% endif %}
    {


    {% if date(user.last_seen_at) > date('-30minutes') %}
      "status": "online",
    {% else %}
      "status": "idle",
    {% endif %}
    "avatar_absolute_url": "http://forum.kozovod.com{{ user.avatar_template|replace({'{size}':'22'}) }}",
    "user_absolute_url": "http://forum.kozovod.com/users/{{user.username_lower}}",
    "user_from_api": {{user|json}}


  }

  {% endif %}
  {% set num = num + 1 %}
{% endfor %}
]

A few notes:

user.username != ‘api_ru’ - I’m using a separate admin user api_ru in Discourse for API calls. This check makes sure it won’t appear in the resulting list of online users.

{% if num <= 11 %} - up to 11 users in the list

http://forum.kozovod.com - don’t forget to change to your own Discourse URL in all links

{% if date(user.last_seen_at) > date(’-30minutes’) %} - users seen in last 30 minutes will be shown with a green circle; the rest will have an orange circle. Adjust this timeframe to your needs.


API Endpoint in WordPress

Create a new API Endpoint in Settings -> API Endpoints in your WordPress admin panel:

A few notes:

Success and Error templates are there for you by default

In the HTTP headers section, don’t forget the following 2 headers:

Content-Type: application/json; charset=utf-8

  • this one will indicate that the API output is JSON

Access-Control-Allow-Origin: *

  • this one will allow for API to be requested from outside of your WordPress domain

API Output

Your new API endpoint will return JSON of the following structure:


Done!

I am afraid we need to keep all stuff that costs :dollar: in the market place category.

who’s online should be trivial as a plugin btw.

Yes, please move to the “marketplace”. I wasn’t sure about it, so used “no category”.

I think so, but I don’t want any Ruby business, so just did it the dirty and quick way :slight_smile: It will do the job until a good plugin is available.

Eventually moved it to the top, otherwise it is not seen with Discourse’s indefinite scroll:


For this to work, just move the <section> tag to the HEAD customization:

I wonder why you need to build an API in Wordpress when you can talk directly to Discourse’s API?

1 Like

That’s because I want to hide my api key.
If the api was not restricted to admins only, I’d call it directly from the user browser.

UPD. Does it make sense? I suppose it is not safe to disclose the api key?

2 Likes

That’s actually a very good point :wink:

1 Like

If I needed to design a whos online thingy I would use an outlet at the bottom of the categories page, and then have a message bus channel that posted once in a while updates to the list.

Presense … correctly… is actually a tricky problem.

Exactly! That’s why I went the easy-way with the hope that sooner or later there will be a complete sophisticated plugin for that done the right way.

No doubt my solution is dirty. But I got it working in < 2 hours - not comparable with outsourcing to a Ruby developer for at least a few days designing and coding at a rate close to $100/hour, as Jeff once mentioned.


UPD. Querying for online presence is enough at a rate of once every few minutes, so a simple Ajax request should suffice for most small and medium forums. The API results are cached in WordPress, so if multiple clients query the WordPress API Endpoint, this does not mean there will be the same number of additional HTTP calls to the Discourse server.

So how did this hack went after the recent update?

This needs WP to work right, if no WP integration, won’t be functional? I’m waiting for the same plugin actually.

Nice job

1 Like

All is working nicely for me after all updates - http://forum.kozovod.com - see the list at the very top of the home page.

Yep, it needs WordPress. That’s only because we can’t query admin-only data from client-side js (you’ll have to put your secret key at risk)… unless it’s made as a plugin. However, I’m not a ruby programmer, so not planned to make it a plugin, unfortunately.

1 Like

I can see that. Wish to add something the same but the complexity seems not at my level yet :smiley: