Building xmpp (jabber) app for Firefox OS - Part1

In the last month everybody is talking about instant messenger. There are some for Firefox OS, but not really good ones. Often you are bound to one service. But with xmpp you can create your account at one of hundreds of servers all over the world. It's just a protocol. A little bit like email. My client talks to my server, my server to the server of my contact and the server of my contact with the client of my contact. You can inform your self about jabber at jabber.org. I would like to build a xmpp (jabber) app for Firefox OS. The problem: I didn't know much about xmpp and I'm not the best javascript developer. The first problem will be solved in the second part of this post, but I want to build the basic structure of the app in the first part. I did read Quick Guide For Firefox OS App Development from Andre Garzia. The book is pretty good, but like explained in the book it isn't always best practice. But it works. So I have build my app a little bit like explained in this book. At the moment I have a Alcatel One Touch Fire with Firefox OS 1.1.0.0-prerelease. The ugliest phone you can imagine, please never buy such a shit. I just use it, cause I guess Firefox OS can maybe the next revolution for mobile OS. It's pretty hard to work with such a phone if you have used a iphone since 4 years.

Name and Logo

Not a really important part, but sometimes I like to start with the design. I'm not very good at such stuff but I like to try. The name of the application is Anne. The logo is pretty easy, I just moved some triangles to build an A on green background. Inkscape was very helpful. For the app I need this logo with 60 and with 128 pixel.

Anne Icon

Sections and Design

The app should be very simple. I want a settings section, were I can edit my account and maybe later some other things. Then there should be a overview over all my contacts and a section were I can read and send messages to a contact. All theses sections will maybe change a little bit in the future.

Anne Design

For the design we use Building Blocks. I think they look very simple and cool. We just make them green with some lines of CSS.

Structure

Here you can see the structure of the app. Like I said above we use the building blocks. You can download them from here. I just need buttons.css, headers.css, input_areas.css, lists.css and there subfolders. All these files are in the directory style. The util.css is also used. I have put these files into style/building-blocks.

anne/
|-- images/
|   |-- icon_128.png
|   `-- icon_60.png
|-- js/
|   |-- account.js
|   |-- app.js
|   |-- contact.js
|   `-- message.js
|-- style/
|   |-- anne/
|   |   `-- images/
|   |       `-- icons/
|   |           |-- back@1.5x.png
|   |           |-- back@2x.png
|   |           |-- back.png
|   |           |-- settings@1.5x.png
|   |           |-- settings@2x.png
|   |           `-- settings.png
|   |-- building-blocks/
|   |   |-- buttons/
|   |   |-- headers/
|   |   |-- input_areas/
|   |   |-- lists/
|   |   |-- buttons.css
|   |   |-- headers.css
|   |   |-- input_areas.css
|   |   |-- lists.css
|   |   `-- util.css
|   `-- anne.css
|-- index.html
`-- manifest.webapp

Manifest

Every app starts with a manifest.webapp file.

{
    "name": "Anne",
    "version": "0.1.0",
    "description": "A simple app to connect via xmpp/jabber",
    "launch_path": "/index.html",
    "permissions": {
        "storage": {
            "description": "Required for storing and retrieving notes"
        }
    },
    "developer": {
        "name": "Gordon Lesti",
        "url": "https://gordonlesti.com"
    },
    "icons": {
        "60": "/images/icon_60.png",
        "128": "/images/icon_128.png"
    }
}

I guess the most parts of this manifest are clear. We give the application a name, a version and a description. The launch_path says Firefox OS were to start the app. In the permissions tag we announce to use the storage. The indexedDB. The give some information about the developer and say were to find our icons.

index.html

The starting point of your application is the index.html.

<!DOCTYPE html>
<html>

<head>
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
    <meta charset="utf-8">
    <title>Anne</title>

    <!-- Building Blocks -->
    <link href="style/building-blocks/headers.css" rel="stylesheet" type="text/css">
    <link href="style/building-blocks/buttons.css" rel="stylesheet" type="text/css">
    <link href="style/building-blocks/lists.css" rel="stylesheet" type="text/css">
    <link href="style/building-blocks/input_areas.css" rel="stylesheet" type="text/css">
    <link href="style/building-blocks/buttons.css" rel="stylesheet" type="text/css">
    <link href="style/building-blocks/util.css" rel="stylesheet" type="text/css">
    <!-- App styles -->
    <link href="style/anne.css" rel="stylesheet" type="text/css">

    <script src="js/contact.js"></script>
    <script src="js/message.js"></script>
    <script src="js/account.js"></script>
</head>

<body role="application">
<!--·························· Contacts ··························-->
<section id="contacts-view" role="region">
    <header>
        <menu type="toolbar">
            <a href="#"><button role="menuitem" id="open-settings-btn"><span class="icon icon-settings">settings</span></button></a>
        </menu>
        <h1>Contacts</h1>
    </header>
    <article class="scrollable">
        <section data-type="list">
            <ul id="contacts-list">
                <!-- here we add the contacts -->
            </ul>
        </section>
    </article>
</section>

<!--·························· Settings ··························-->
<section id="settings-view" role="region" class="skin-dark display-none">
    <header>
        <menu type="toolbar">
            <a href="#"><button role="menuitem" id="close-settings-btn" >done</button></a>
        </menu>
        <h1>Settings</h1>
    </header>
    <article class="content">
        <header>
            <h2>Account</h2>
        </header>
        <form onsubmit="return false;">
            <p>
                <input id="jid-input" type="text" required="required" placeholder="user@jabber.org"/>
            </p>
            <p>
                <input id="password-input" type="password" required="required" placeholder="password"/>
            </p>
            <p>
                <input id="bosh-input" type="text" required="required" placeholder="http://jabber.org/protocol/httpbind"/>
            </p>
            <p>
                <button id="save-account">Save</button>
            </p>
        </form>
    </article>
</section>

<!--·························· Messages ··························-->
<section id="messages-view" role="region" class="display-none">
    <header>
        <a href="#" id="close-messages-btn"><span class="icon icon-back">back</span></a>
        <h1 id="contact-name">Contact Name</h1>
    </header>
    <article>
    <article>
        <!--here we add the messages-->
        <div id="messages-list" class="scrollable"></div>
        <div style="clear:both;"></div>
        <form role="search" id="messages-compose-form" class="bottom messages-compose-form">
            <button id="messages-send-button" type="submit">Send</button>
            <p>
                <textarea id="messages-subject-input" placeholder="Subject"></textarea>
            </p>
        </form>
    </article>
</section>

<script src="js/app.js"></script>
</body>

</html>

In the head we load some CSS of Building Blocks and our own and also load our javascript files contact.js, message.js and account.js. These are only models. I guess Andre Garzia said in his book, it would be better to use something like require.js and I can only agree. That will be used in some of the next parts of this post. The section with id contacts-view is the overview for your contacts. The section with id settings-view is the settings area and the section with id messages-view is the area were we will read and send messages to one of your contacts. I have given many elements an id cause we need them later. At the end of the body I load app.js.

CSS

The Building Blocks style the most part of the application, but some lines of CSS are also in style/anne.css.

/* Icon definitions */
section[role="region"] > header:first-child .icon.icon-settings {
    background-image: url(anne/images/icons/settings.png);
}

.display-none {
    display: none;
}

section[role="region"] > header:first-child {
    background-color: #17f972;
}

section[role="region"] > header:first-child > a:not([aria-disabled="true"]):active:after,
section[role="region"] > header:first-child > button:not(:disabled):active:after,
section[role="region"] > header:first-child > a:not([aria-disabled="true"]):hover:after,
section[role="region"] > header:first-child > button:not(:disabled):hover:after,
section[role="region"] > header:first-child menu[type="toolbar"] a:not([aria-disabled="true"]):hover,
section[role="region"] > header:first-child menu[type="toolbar"] button:not(:disabled):hover,
section[role="region"] > header:first-child menu[type="toolbar"] a:not([aria-disabled="true"]):active,
section[role="region"] > header:first-child menu[type="toolbar"] button:not(:disabled):active  {
    background-color: #0edc61 !important;
}

section[role="region"] > header:first-child .icon.icon-back {
    background-image: url(anne/images/icons/back.png);
}

/*
  Bottom bar for sending SMS
*/

.messages-compose-form[role="search"] {
    transition: transform .2s ease-in;
    background: white;
}

form.bottom[role="search"].messages-compose-form {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
}

.message-contact {
    margin: 0.5rem 0 0.5rem 0;
    padding: 0.5rem 0.5rem 0.5rem 1.0rem;
    max-width: 80%;
    color: #ffffff;
    float: left;
    background-color: lightblue;
    clear: both;
}

.message-me {
    margin: 0.5rem 0 0.5rem 0;
    padding: 0.5rem 1.0rem 0.5rem 0.5rem;
    max-width: 80%;
    color: #ffffff;
    float: right;
    background-color: lightcoral;
    clear: both;
}

Here we set the icon for the settings. I use a class display-none to hide the not active sections. I guess there is a much better way to do so. In the examples of Building Blocks they get it sliding from right to left and so, but I didn't get it working well. So I just hide a section if the section isn't active. Then I add some line to replace orange with green for the headers. Also for the back-icon. Then there is some styling for the send button and for the messages boxes.

Some Models

A message just have a content and knows the contact how has written the message. The file js/message.js.

function Message(contact, content) {
    this.contact = contact;
    this.content = content;
}

A Contact is the model for an contact with a jabberID and maybe an image. We save the messages between me and my contact as an array. The file js/contact.js.

function Contact() {
    this.name = "Gordon Lesti";
    this.jid = "gordon.lesti@jabber.de";
    this.img = "http://1.gravatar.com/avatar/1a3cb8af5f5c0874ec777febdb606d6f?s=40&d=identicon&r=G";
    this.messages = [
        'jojo'
    ];
}

Now the account. We will put all contacts into the account model and will save also the jid, password and the bosh. Just to explain, I'm not 100% secure about saving password plain into the indexedDB. Other Firefox core applications like the calendar do it like this, so I hope it is save. I guess bosh isn't really clear, bosh is the address to a bosh server. In the second part of this post we will use Strophe.js to connect our application via xmpp with a server. With a desktop client like Empathy on Debian for example, you didn't need such a thing like a bosh server. But if you just have a browser that only speaks HTTP, you have to transform your requests over a bosh server. As Firefox OS Browser doesn't support Flash we need a bosh server that supports Cross-domain requests (CORS). Just ask you jabber server support and they will help you. Now the js/account.js.

var dbName = "xmppAccounts";
var dbVersion = 1;

var db;
var request = indexedDB.open(dbName, dbVersion);

request.onerror = function (event) {
    console.error("Can't open indexedDB!!!", event);
};
request.onsuccess = function (event) {
    db = event.target.result;
    console.log("Database opened ok");
}
request.onupgradeneeded = function (event) {
    db = event.target.result;

    if (!db.objectStoreNames.contains("xmppAccounts")) {
        var objectStore = db.createObjectStore("xmppAccounts", {
            keyPath: "jid",
            autoIncrement: false
        });
        console.log("db installed");

        objectStore.add(getSampleAccount());
    }
}

function Account(jid, password, bosh) {
    this.jid = jid;
    this.password = password;
    this.bosh = bosh;
    this.contacts = [];
}

function getAllAccounts(accounts, Callback) {
    var objectStore = db.transaction("xmppAccounts").objectStore("xmppAccounts");

    objectStore.openCursor().onsuccess = function (event) {
        var cursor = event.target.result;
        if (cursor) {
            accounts.push(cursor.value);
            cursor.continue();
            console.log("Add Account " + cursor.value.jid);
        } else {
            Callback();
        }
    };
}

function saveAccount(inAccount) {
    var transaction = db.transaction(["xmppAccounts"], "readwrite");
    console.log("Saving memo");

    transaction.oncomplete = function (event) {
        console.log("All done");
    };

    transaction.onerror = function (event) {
        console.error("Error saving account:", event);
    };

    var objectStore = transaction.objectStore("xmppAccounts");

    var request = objectStore.put(inAccount);
    request.onsuccess = function (event) {
        console.log("Account saved");
    };
}

function getSampleAccount() {
    var sampleAccount = new Account("user@jabber.org", "password", "http://jabber.org/protocol/httpbind");
    var zaphod = new Contact();
    zaphod.name = "Zaphod Beeblebrox";
    zaphod.messages = [
        new Message(null, 'You\'re looking for the Ultimate Question.'),
        new Message(zaphod, 'Yep.'),
        new Message(null, 'You.'),
        new Message(zaphod, 'Me.'),
        new Message(null, 'Why?'),
        new Message(zaphod ,"No, I tried that: Why? 42. Doesn't work.")
    ];
    zaphod.img = "images/user/zaphod.png";

    var arthur = new Contact();
    arthur.name = "Arthur Dent";
    arthur.messages = [
        new Message(null, 'Wash your filthy hands!'),
        new Message(null, ' Don\'t panic... don\'t panic...'),
        new Message(arthur, 'So this is it. We\'re gonna die.'),
        new Message(null, "Yeah. We're gonna die. ")
    ];
    arthur.img = "images/user/arthur.png";
    sampleAccount.contacts = [zaphod, arthur];
    return sampleAccount;
}

The application

At the end the application. I didn't have enough power to explain anymore, but I hope most parts are clear. Like I said at the beginning, I'm a bad javascript developer. At the beginning I initialize the most elements of the DOM, for example buttons and sections to add EventListener. At the first time I had pretty much problems with the asynchronous API of indexedDB. If you need the objects of the indexedDB in following code you should use a callBack function to get the code in one line. Here js/app.js

var openSettingsButton,
    closeSettingButton,
    openMessagesButton,
    closeMessagesButton,
    openContactsButton,
    closeContactsButton,
    messageSendButton,
    accountSaveButton,
    messageInput,
    jidInput,
    passwordInput,
    boshInput,
    settingsView,
    messagesView,
    contactsView,
    messagesList,
    contactsList,
    contactName;

var currentContact;
var accounts = [];

function init() {
    if (!db) {
        switchToSettings();
    } else {
        getAllAccounts(accounts, switchToSettingsOrContacts);
    }
}

function switchToSettingsOrContacts() {
    if (accounts.length == 0) {
        switchToSettings();
    } else {
        enableCloseSettingsButton();
        renderContactsView();
    }
}

function switchToSettings() {
    renderSettings();
    contactsView.classList.add('display-none');
    settingsView.classList.remove('display-none');
}

function switchToContacts() {
    renderContactsView();
    settingsView.classList.add('display-none');
    contactsView.classList.remove('display-none');
}

function renderContactsView() {
    contactsList.innerHTML = '';
    if (accounts.length > 0) {
        var account = accounts[0];
        var i = 0;
        for (i = 0; i < account.contacts.length; i++) {
            addContactItem(account.contacts[i]);
        }
    }
}

function addContactItem(contact) {
    var li = document.createElement('li');
    var aside = document.createElement('aside');
    aside.className = 'pack-end';
    var img = document.createElement('img');
    img.setAttribute('src', contact.img);
    aside.appendChild(img);
    li.appendChild(aside);
    var a = document.createElement('a');
    a.setAttribute('href', '#');
    a.addEventListener('click', function(event) {
        renderMessagesView(contact);
        contactsView.classList.add('display-none');
        messagesView.classList.remove('display-none');
    })
    var pName = document.createElement('p');
    var textName = document.createTextNode(contact.name);
    pName.appendChild(textName);
    a.appendChild(pName);
    var pLastMessage = document.createElement('p');
    var lastMessage = '';
    if (contact.messages.length > 0) {
        lastMessage = contact.messages[contact.messages.length-1].content;
    }
    var textLastMessag = document.createTextNode(lastMessage);
    pLastMessage.appendChild(textLastMessag);
    a.appendChild(pLastMessage);
    li.appendChild(a);
    contactsList.appendChild(li);
}

function renderMessagesView(contact) {
    currentContact = contact;
    messagesList.innerHTML = '';
    contactName.childNodes[0].nodeValue = contact.name;
    for (var i in contact.messages) {
        var message = contact.messages[i];
        renderMessage(message);
    }
}

function renderMessage(message) {
    var p = document.createElement('p');
    if (currentContact == message.contact) {
        p.className = 'message-contact';
    } else {
        p.className = 'message-me';
    }
    var textContent = document.createTextNode(message.content);
    p.appendChild(textContent);
    messagesList.appendChild(p);
}

function renderSettings() {
    if (accounts.length > 0) {
        var account = accounts[0];
        jidInput.value = account.jid;
        passwordInput.value = account.password;
        boshInput.value = account.bosh;
    }
}

function enableCloseSettingsButton() {
    closeSettingButton.classList.remove('display-none');
}

window.onload = function() {
    // buttons
    openSettingsButton = document.getElementById('open-settings-btn');
    closeSettingButton = document.getElementById('close-settings-btn');
    openMessagesButton = document.getElementById('open-messages-btn');
    closeMessagesButton = document.getElementById('close-messages-btn');
    openContactsButton = document.getElementById('open-contacts-btn');
    closeContactsButton = document.getElementById('close-contacts-btn');
    messageSendButton = document.getElementById('messages-send-button');
    messageInput = document.getElementById('messages-subject-input');
    accountSaveButton = document.getElementById('save-account');
    jidInput = document.getElementById('jid-input');
    passwordInput = document.getElementById('password-input');
    boshInput = document.getElementById('bosh-input');

    // views
    settingsView = document.getElementById('settings-view');
    messagesView = document.getElementById('messages-view');
    contactsView = document.getElementById('contacts-view');

    // lists
    contactsList = document.getElementById('contacts-list');
    messagesList = document.getElementById('messages-list');

    contactName = document.getElementById('contact-name');

    // add EventListeners
    openSettingsButton.addEventListener('click', function () {
        switchToSettings();
    });
    closeSettingButton.addEventListener('click', function () {
        switchToContacts();
    });
    closeMessagesButton.addEventListener('click', function() {
        messagesView.classList.add('display-none');
        contactsView.classList.remove('display-none');
        currentContact = null;
    })
    // message will be send and stored
    messageSendButton.addEventListener('click', function() {
        var message = new Message(null, messageInput.value)
        currentContact.messages.push(message);
        renderMessage(message);
        messageInput.value = '';
    })
    // save the account
    accountSaveButton.addEventListener('click', function() {
        var jid = jidInput.value;
        var password = passwordInput.value;
        var bosh = boshInput.value;
        var account = new Account(jid, password, bosh);
        saveAccount(account);
        accounts.push(account);
        enableCloseSettingsButton();
    })

    init();
}

I know the code is currently at an terrible condition.

Next Previous