HN-Notifier

A native HTML app for Hacker News Notifications on OS X

 

This tutorial details the development process for an HTML based, native app for OS X which notifies users about new articles on the Hacker News frontpage.
If you would simply like to get the app instead of going through the entire tutorial, you can download the HN-Notifier app here.
Please note that this is an unofficial app and in no way endorsed by YCombinator.

The HN-Notifier app is a very simple notification app, it starts up with a list of current articles on the Hacker News frontpage and then notifies the user when new articles are added.

The app's functionality can be boiled down to a few simple steps.

  • Use the unofficial Hacker News API to display frontpage articles as a jQuery Mobile list.
  • Add some jQuery Mobile UI elements and provide some additional functionality to clear the list, display info, etc
  • Wrap our app in a Webkit WebView using macgap, with Growl notifications as well as dock badges so that the app is more closely integrated with the OS.

All you really need in order to follow this tutorial, is a basic understanding of Javascript, pretty much everything else is fairly easy to pick up as we go along.

Getting the article list

At the apps core, we know that we need to have a list of articles, so let's start by designing a quick mockup.

Open up the mobjectify editor.
If you're not logged in to an account, the editor will start up with a sample page. Delete it and add a fresh page using the page button on the left sidebar.

Let's also rename the page from the default 'Page-1', to articles

Add a list from the available controls in the left panel, and add some dummy articles, click on the refresh icon on top of the device view to preview changes

Well, it would be better if the list took up all available space, and also a header would give it more context, so change the list type from inset to fill screen, and click on the 'Add Header' button to give the page a header as 'Articles'

Also, jQuery Mobile styles links in lists differently from plain list items, so click on each list item you've added and add a dummy url in the link field to get a feel of how the links will eventually be styled.

Seems, alright for our initial mockup, let's add functionality to pull in the actual articles and update our list.

We're going to use the unofficial Hacker News API to get a list of articles, currently on the frontpage.
Getting the list in JSON format is as simple as making a GET request to http://api.ihackernews.com/page

The only problem if we used this approach directly, is that the browser won't let us read the data returned by a GET request to another domain, due to cross domain security policies, so we will have to use a JSONP callback function to actually be able to read the articles returned.
The API allows us to do that easily, by passing the parameter format set to jsonp
Since we have jQuery included in our app already, we can use jQuery's ajax method to make the jsonp request.

Before we write any actual code, we need a way to target the list from our javascript, we can do this by giving it an id.
In the list details in the center panel, click on More and enter article_list as its id.

Now we can switch to the code tab and write code to actually make a jsonp call using jQuery.ajax when the 'pageinit' event is triggered on the articles page

// Variable to store the list of unread articles
var article_list = [];
// Store ids of articles so that we do not add duplicates to the list
var article_ids = [];
// Store a reference to our list in DOM, as a global variable
var $article_list;

/** Callback function to read data returned by api call */
function api_callback(data) {
    if (data.hasOwnProperty('items') && data.items.length) {
        data = data.items;
        // Insert articles in reverse order to the front of our list
        // so that freshly added articles end up on top
        for (var i=data.length-1; i>=0; i--) {
            // Add article only if not processed before
            if (article_ids.indexOf(data[i].id) === -1) {
                article_list.unshift({id:data[i].id, article_url:data[i].url, title:data[i].title});
                article_ids.push(data[i].id);
            }
        }
        // Refresh the list
        refresh_list();
    }
}

/** Call the unofficial hacker news API to get frontpage articles */
function get_frontpage() {
    $.ajax({
        url: 'http://api.ihackernews.com/page?format=jsonp',
        dataType: 'jsonp',
        success: api_callback
    });
}

/** Refresh the jQuery Mobile list */
function refresh_list() {
    // Empty existing items in the list
    $article_list.empty();
    // Reinsert list items from our array
    for (var i=0; i<article_list.length; i++) {
        $article_list.append('<li><a href="' + article_list[i].article_url 
            + '" data-index="' + i 
            + '" data-comments="http://news.ycombinator.com/item?id=' + article_list[i].id + '">' 
            + article_list[i].title + '</a></li>');
    }
    // Refresh the listview
    $article_list.listview('refresh');
}

// Bind to the pageinit function of the articles page
$(document).delegate('#articles', 'pageinit', function(){
    // Set our global variable
    $article_list = $('#article_list');
    // Refresh the list
    refresh_list();
    // Fetch articles
    get_frontpage();
});

The code only runs while you have the Code tab selected, so you can select Design to go back to your mockup at any time.

Click on the refresh icon, and you should see the list with articles currently on the frontpage of Hacker News, and clicking the list items takes you to the article.
You can preview the changes here

If you look at the code for the refresh_list function, we have added the index of the article in our array and the comment url as HTML5 data attributes.
We want the users to have the option be able to go to the comments page, and we need to remove visited articles from our list, or else it doesn't really serve its purpose as a notification app if all articles stay there even after being read.

jQuery Mobile provides a vclick event to allow us to capture mouse clicks as well as touch based interactions with a single event. Let's bind to the vclick event on our list items and remove the item once it has been clicked.

We will be making the articles load in a new tab instead of the current one, and we can make the comments page load if the user presses the meta key (Cmd Key on Mac, Ctrl key on Win/Linux) while clicking.
This is probably bad UX for purely web based apps, since users may be expecting links to open in the same tab and pressing Cmd or Ctrl while clicking is usually associated with opening a new tab in most browsers, but we're going to convert our app into a native app down the line, and when you consider the fact that the WebView doesn't have browser chrome, it makes a lot more sense to just open the links separately.

So we add a function article_click which we bind to the vclick event within the pageinit handler.

/** Remove visited articles from notification list */
function article_click(event) {
    // Find the 'a' tag within the li
    var $el = $(this).find('[data-index]'),
        index = $el.data('index');
    // Remove the article from the list
    article_list.splice(index, 1);
    refresh_list();
    // Load the comments page if Cmd key was pressed (Ctrl on Windows/Linux)
    if (event.metaKey) {
        window.open($el.data('comments'));
    }
    else {
        window.open($el.attr('href'));
    }
    event.preventDefault();
}

// Bind to the pageinit function of the articles page
$(document).delegate('#articles', 'pageinit', function(){
    // Set our global variable
    $article_list = $('#article_list');
    $article_list.delegate('li', 'vclick', article_click);
    // Refresh the list
    refresh_list();
    // Fetch articles
    get_frontpage();
    setInterval(get_frontpage, 300000);
});

We have also added a call to setInterval, so that the frontpage articles are refreshed every five minutes

The app is pretty much functional at this point, but it could definitely use a better UI.
We should also give the user an option to clear the existing list so that they do not have to click on uninteresting articles to clear them out.

Enhancing the interface

Let's start off by modifying the theme being used.
You can design your own custom themes in the Mobjectify theme editor, or import an already existing theme, such as one made using the jQuery Mobile Themeroller.

Here, we're going to use Glossify, which is similar to the default theme, except with a glossy finish instead of matt, and adds two extra swatches for red and green.
To use Glossify, you will need to download it first and then upload it as a custom theme.

Click on the Theme link in the editor menu, and choose Use Custom CSS.
Here upload the Glossify css file and the existing swatches will be replaced by those in the css file

Go back to the editor, you will notice that the design already uses the new theme.
The preview window will not run the code in design mode, so the actual article titles are not loaded, and you will instead see the dummy values you entered earlier.

Go to the page header in the center panel, and click the '#' icon to change its theme styling. Select the 'F' swatch to make it red in color.
Also, click on Add footer to add a footer bar, and in the footer details, click on Add navbar

We now add two buttons to the navbar, one labeled 'Clear', to clear the list, and another labeled 'About', to link to an 'About' page, and give them both appropriate icons.
The footer would look better if it was always visible at the bottom, even if the user is not scrolled all the way down, so set the footer position to Fixed
We will need to add the functionality for the clear button in the code, so click on it in the center panel of the editor and give it an id of 'clear'.

The About button will link to another page, so click on it and enter #about in the Link To field.

We now need to add the about page that we linked our button to, so add another page using the leftmost panel, and give it the name about
Add a header and footer as we did for the previous page, except here we make the first footer navbar button link back to #articles, and the second button called 'About' is just for consistency with the UI of our earlier page.

Click on Content in the leftmost panel to add text to the page.
Here we add some info about the app, its use, etc.

Switch to the code tab to add functionality for our Clear button, which we can do during pageinit

// Bind to the pageinit function of the articles page
$(document).delegate('#articles', 'pageinit', function(){
    // Set our global variable
    $article_list = $('#article_list');
    $article_list.delegate('li', 'vclick', article_click);
    // Refresh the list
    refresh_list();
    // Fetch articles
    get_frontpage();
    $('#clear').bind('vclick', function() {
        article_list = [];
        refresh_list();
    });
    setInterval(get_frontpage, 300000);
});

That's it, our app is pretty much ready in the browser at this point. Time to go native.

Going native using macgap

Macgap is based on phonegap-mac and allows you to run HTML/CSS/JS based code as a native app on Mac OS X in a WebView.
Usage of macgap is really simple, and all it requires is that you provide an index.html file which will get loaded into the WebView.

To get started, install the macgap generator using

gem install macgap

Since macgap, simply loads up the index.html file in a WebView, you can choose from various options regarding the availability of your app

  • Fully offline - Your app HTML as well as all its resources can be packaged with your app for distribution
  • Fully online - Make index.html refresh to an online url
  • Offline with online updates - You can use manifest files to make the app cacheable for offline availability. This allows the user to download fresh resources only if updated.

Use the Export link in the editor toolbar to get the code for our app as a zipped file.
Macgap uses the folder name as the application name, so after unzipping the files, make sure they're in a folder named HN-Notifier.

For testing the app, you're going to be running the HTML from local files, but we also want to be able to easily switch to an online URL as the source of our app once ready for distribution.
So, change the filename of mobjectify.html in the unzipped file to hnnotifier.html, and add an index.html which simply refreshes to the hnnotifier.html file.
Our index.html would have the following contents

<!DOCTYPE HTML>
<html>
    <head>
        <title>HN-Notifier</title>
        <meta http-equiv="refresh" content="0;url=hnnotifier.html">
        <style>
            h1 {
                margin-top:21%;
                color: #E9E9E9;
                font-family: Helvetica;
                font-size: 2.5em;
                text-align: center;
            }
        </style>
    </head>
    <body>
        <h1>Connecting ...</h1>
    </body>
</html>

This will allow us to simply replace the url in the meta tag to an online url to be able to get our app from an online source.
We're going to run our app without making any OS specific changes for now, so we're ready to turn it into an app.

Open up a shell terminal and go to the parent folder of our HN-Notifier folder and simply run the command

macgap build HN-Notifier

You should now see HN-Notifier.app in the folder and if you run it, it's our app running in a WebView.

If you play around with the app, pretty soon it's apparent that loading the linked articles in the WebView isn't really good UX due to lack of the browser chrome.
We need to open up the linked articles in the user's browser so that they can be interacted with in a way that the user expects.

Before you start integrating macgap into your code, make sure that it's fully tested in the browser. The WebView doesn't provide you developer tools you would find in a standard browser, so any bugs in your code are going to lead you down the slow path to chaos leaving your code peppered with growl notifications, or worse yet, alerts, to display debugging information.

Macgap provides us with a macgap object, which can be referenced from our Javascript code for closer integration with the OS.
A full list of the functionality provided by the macgap object is available in its documentation, but as far as our app is concerned, we're going to limit ourselves to just the following.

  • macgap.app.open to open the links in the user's browser
  • macgap.growl.notify to notify the user of unread articles
  • macgap.dock.badge to display a count of unread articles in the dock.

We add a growl notification at the end of our api callback function to display notifications every time unread articles are added. So our function looks like

/** Callback function to read data returned by api call */
function api_callback(data) {
    var notify = false;
    if (data.hasOwnProperty('items') && data.items.length) {
        data = data.items;
        // Insert articles in reverse order to the front of our list
        // so that freshly added articles end up on top
        for (var i=data.length-1; i>=0; i--) {
            // Add article only if not processed before
            if (article_ids.indexOf(data[i].id) === -1) {
                notify = true;
                article_list.unshift({id:data[i].id, article_url:data[i].url, title:data[i].title});
                article_ids.push(data[i].id);
            }
        }
        // Refresh the list
        refresh_list();
        if (notify) {
            macgap.growl.notify({
                title:'Hacker News Update',
                content: '' + article_list.length + ' unread article'
                    + (article_list.length > 1?'s':'') + ' in notification list'
            });
        }
    }
}

We similarly modify the refresh_list function so that the count displayed in the dock is also updated whenever the list is refreshed

/** Refresh the jQuery Mobile list */
function refresh_list() {
    // Empty existing items in the list
    $article_list.empty();
    // Reinsert list items from our array
    for (var i=0; i<article_list.length; i++) {
        $article_list.append('<li><a href="' + article_list[i].article_url 
            + '" data-index="' + i 
            + '" data-comments="http://news.ycombinator.com/item?id=' + article_list[i].id + '">' 
            + article_list[i].title + '</a></li>');
    }
    if (article_list.length) {
        macgap.dock.badge = String(article_list.length);
    } else {
        macgap.dock.badge = '';
    }
    // Refresh the listview
    $article_list.listview('refresh');
}

And finally, modify our article_click function to replace the code to open the link in a new tab, with code to open the link in the user's browser.

/** Remove visited articles from notification list */
function article_click(event) {
    // Find the 'a' tag within the li
    var $el = $(this).find('[data-index]'),
        index = $el.data('index');
    // Remove the article from the list
    article_list.splice(index, 1);
    refresh_list();
    // Load the comments page if Cmd key was pressed (Ctrl on Windows/Linux)
    if (event.metaKey) {
        macgap.app.open($el.data('comments'));
    }
    else {
        macgap.app.open($el.attr('href'));
    }
    event.preventDefault();
}

Build the app again and this time, we actually have notifications and integrated dock badges.
Our hnnotifier.html file will no longer run in the browser though, due to a missing macgap object.

Let's add a final bit of polish to our app.
If you click on HN-Notifier in the system menu, and select 'About Macgap', it displays some placeholder credits. We can change this to display licensing information instead, and we're also going to change the default icon for the app and use our own icon instead.

The HN-Notifier.app created by Macgap is an OS X bundle file, which is basically a folder which the OS presents to us as a file so that applications can be bundled easily.
The credits are loaded up from Contents/Resources/en.lproj/Credits.rtf within the HN-Notifier.app folder, so simply replace the contents of this file with our custom info, which in the case of the HN-Notifier app is this file.

The icon used for the app can be changed by putting a file called application.png in our folder before building the app, and the application.png image will be used as the icon instead.

Now that our app is ready, we can change the refresh url in index.html to point to the location where our app would be online.
Depending on the type of your app, you could of course distribute it as purely offline or add a cache manifest to get offline usage as well as instant updates.

That's it, if you're going to distribute your app, you may want to package it into a dmg file.
Download the finished HN-Notifier app here.

If you have any queries, comments, suggestions, etc, leave us a comment below, we would love to hear from you.

 

comments powered by Disqus