Thursday, August 15, 2013

The current state of plaforms: research and testing

Candidate #1: Chrome

Once I knew I was going for some type of raw tcp socket support an extensive google search was the next step. The first thing I stumbled upon was Chrome's packaged app Network Communications section. Naturally I dropped the research stage and went straight to hacking.
Packaged apps deliver an experience as capable as a native app, but as safe as a web page. Just like web apps, packaged apps are written in HTML5, JavaScript, and CSS. But packaged apps look and behave like native apps, and they have native-like capabilities that are much more powerful than those available to web apps.

Getting started

I already knew from some work on my chrome plugin that developing extensions for Chrome is as easy as it gets. Throw together a manifest.json that has the correct permissions, and you're allowed to use the API.

{
  "manifest_version": 2,
  "version": "0.1",
  "name": "SchizIRC Chrome",
  "app": {
    "background": {
      "scripts": ["main.js"]
    }
  },
  "permissions": [{
    "socket": [
      "tcp-connect:*:6667",
      "tcp-listen:*:6667"
      ]
    }]
}

Main.js contains nothing fancy, It just listens for the onLaunched event and creates the main window with some predefined bounds.

chrome.app.runtime.onLaunched.addListener(function() {

  chrome.app.window.create('index-chrome.html', {
    bounds: {
      width: 980,
      height: 650
    }
  }, function(createdWindow) {
    appWindow = createdWindow.dom;

  });

});

Loading a chrome app in developer mode is as easy as it gets as well:
  • Visit chrome://extensions in your browser
  • Ensure that the Developer mode checkbox in the top right-hand corner is checked.
  • Click Load unpacked extension… to pop up a file-selection dialog.
  • Find the directory where you cloned SchizIRC into and select it.
  • Press 'Launch'
Note that regular CTRL+R, F5 or even window.location.reload() won't work, when developing a packaged app, but the main app window has a 'reload app' menu item in the contextmenu that does this. You will also be able to use the full Webkit Inspector from this menu.
I threw together a basic layout using Divshot; Some tabs, a title, an input field and a button. This was enough to get me started on where I wanted to go. My app also got a working title: SchizIRC (because why not)


divshot layouting
Divshot

After taking all the assets offline (required for a chrome app, and needed to be done anyway for future packaging) I ended up with this directory structure:

manifest.json
index-chrome.html
main.js
index.html
css\bootstrap-combined.min.css
css\google-bootstrap.css
css\schizirc.css
js\Irc.js
js\controller.js
js\gui.js
js\vendor\mootools-more.js
js\vendor\mootools-core.js
js\vendor\mustache.js
js\vendor\bootstrap.min.js

Time to get classy

As shown above, I threw down some classes: Irc.js, Controller.js (which will server as the main kickstarter for the app, already eyeballing we're going to have several app wrappers / frameworks here that need a common entry point) and Gui.js, which will bind DOM events to my app's events. Code says more than words:
/** 
 * Controller.js : Generic entry point for the app.
 */
Controller = new Class({
 gui: false,
 server: false,
 config: { // some default config options i've prepared, these will be overwritten by the config in the future
  server: 'irc.freenode.net',
  port: '6667',
  defaultChannels: '#schizirc',
  nick: 'SchizoIRC',
  userName: 'SchizoIRC',
  realName: 'SchizoIRC v0.01',
  appVersion: 'SchizoIRC v0.01',
  finger: 'omnomnom',
  password: null, 
  logging: true
 },

 initialize: function() {
  console.log("IRC Client initted." );
  this.irc = new IRC(this.config);
  this.gui = new Gui();
  this.irc.connect();
 }
});

window.onload = function() {
 window.app = new Controller();
};
User interface bindings are handled by the User Interface Class (which needs some refactoring for the future but will do for now)
/** 
 * Gui.js implements DOM bindings to change the user interface
 * Will delegate user interaction to the app components that need it and implement the base tab view
 */
Gui = new Class({
 Implements: [Events], // allow this class to fire and receive events.

 initialize: function() {
  window.addEvent('mousedown:relay(.nav-tabs li)', this.switchTabs); // handle all clicks on tab labels
  window.addEvent('/tab/changed', this.showActiveTab); // a custom event channel fired when a tab has changed
  window.addEvent('/tab/change', this.switchTab); // a custom event channel that changes a tab to $name
  this.showActiveTab('home'); // always show the home tab.
 },

 switchTabs: function() {
  $$(".nav-tabs li").removeClass('active'); // deactivate all tab labels.
  $(this).addClass('active'); // activate the tab label that was clicked
  window.fireEvent('/tab/changed', this.getAttribute('data-tab')); // fire our tab changed event.
 },

 switchTab: function(newTab) {
  console.log("Change tab to: ", newTab, $$("li[data-tab="+newTab+"]")); 
  $$(".nav-tabs li").removeClass('active'); // hide all tab labels.
  $$("li[data-tab="+newTab+"]").addClass('active'); // show our new tab
  window.fireEvent('/tab/changed', newTab); // notify that the active tab has changed.
 },

 showActiveTab: function(name) {
  $$('section[data-tab]').hide(); // hide all tab content panels
  $$('section[data-tab='+name+']').show(); // show the one passed as name
  $$('section[data-tab='+name+'] input[type=text]')[0].focus(); // focus the text field in the tab
 }
});

Now on to the real deal, the IRC class. This will hold the connection object, the info about the server, channels, etc.

IRC = new Class({
 Implements: [Options, Events], 

 options: { // set some default IRC options that will be overwritten with options passed from class Controller
  server: 'irc.freenode.net',
  port: '6667',
  defaultChannels: '#schizirc',
  nick: 'SchizoIRC',
  userName: 'SchizoIRC',
  realName: 'SchizoIRC v0.01',
  appVersion: 'SchizoIRC v0.01',
  finger: 'omnomnom',
   autoJoinOnInvite: true
  },

  connection: false,
  server: false,
  channels: false,

  initialize: function(options) {
   this.setOptions(options, this.options); // merge the passed options with this.options.
  if(!this.connection) {                  // create a new IRC Connection object that gets the current options.
    this.connection = new IRC.Connection(this.options);
   }
  },

  connect: function() {
   this.connection.connect();
  },

  disconnect: function() {
   this.connection.disconnect();
  }

 });

Wednesday, August 14, 2013

Building an IRC client in HTML5 with native sockets

We're half way into 2013, and it's about time for me to test the state of HTML5 and Javascript in a real world application again (something I try to do regularly) and maybe even push some of my own limits while I'm at it.

To see how far I can get, I've decided to build an IRC client and will try to document my progress, challenges and findings here.

Some starting decisions:
  • I'd like it to run in my favorite browser (Chrome), On the desktop, on Android and iOS if at all possible.
  • I do not want to use websockets and a server to proxy socket traffic to websockets or socket.io. Since it's 2013 I'm going to try to use direct TCP socket API's where available to talk directly to an IRC server, or even write my own implementation where needed.
  • I want to use plain HTML5 and JS, No over-the-top UI frameworks or custom html tags and widgets.
  • If this runs on a tablet/phone, I want to package it to that platform's native format (e.g. .apk / .ipa ) and be able to deploy it into their respective appstores.

Since this is a private project, I can choose my own frameworks, and the frameworks of choice are:
Next stop: Platform research and demo's.