making it librejs compliant master wip-librejs
authork054 <k@fsf.org>
Fri, 18 Mar 2016 19:20:16 +0000 (15:20 -0400)
committerk054 <k@fsf.org>
Fri, 18 Mar 2016 19:20:16 +0000 (15:20 -0400)
client/assets/kiwi.js~ [new file with mode: 0644]
client/assets/kiwi.min.js~ [new file with mode: 0644]
client/index.html~ [new file with mode: 0644]
client/weblabels.html [new file with mode: 0644]
client/weblabels.html~ [new file with mode: 0644]
weblabels.html [new file with mode: 0644]

diff --git a/client/assets/kiwi.js~ b/client/assets/kiwi.js~
new file mode 100644 (file)
index 0000000..639e6b2
--- /dev/null
@@ -0,0 +1,8551 @@
+(function (global, undefined) {
+
+// Holds anything kiwi client specific (ie. front, gateway, _kiwi.plugs..)\r
+/**\r
+*   @namespace\r
+*/\r
+var _kiwi = {};\r
+\r
+_kiwi.misc = {};\r
+_kiwi.model = {};\r
+_kiwi.view = {};\r
+_kiwi.applets = {};\r
+_kiwi.utils = {};\r
+\r
+\r
+/**\r
+ * A global container for third party access\r
+ * Will be used to access a limited subset of kiwi functionality\r
+ * and data (think: plugins)\r
+ */\r
+_kiwi.global = {\r
+    build_version: '',  // Kiwi IRC version this is built from (Set from index.html)\r
+    settings: undefined, // Instance of _kiwi.model.DataStore\r
+    plugins: undefined, // Instance of _kiwi.model.PluginManager\r
+    events: undefined, // Instance of PluginInterface\r
+    rpc: undefined, // Instance of WebsocketRpc\r
+    utils: {}, // References to misc. re-usable helpers / functions\r
+\r
+    // Make public some internal utils for plugins to make use of\r
+    initUtils: function() {\r
+        this.utils.randomString = randomString;\r
+        this.utils.secondsToTime = secondsToTime;\r
+        this.utils.parseISO8601 = parseISO8601;\r
+        this.utils.escapeRegex = escapeRegex;\r
+        this.utils.formatIRCMsg = formatIRCMsg;\r
+        this.utils.styleText = styleText;\r
+        this.utils.hsl2rgb = hsl2rgb;\r
+\r
+        this.utils.notifications = _kiwi.utils.notifications;\r
+        this.utils.formatDate = _kiwi.utils.formatDate;\r
+    },\r
+\r
+    addMediaMessageType: function(match, buildHtml) {\r
+        _kiwi.view.MediaMessage.addType(match, buildHtml);\r
+    },\r
+\r
+    // Event managers for plugins\r
+    components: {\r
+        EventComponent: function(event_source, proxy_event_name) {\r
+            /*\r
+             * proxyEvent() listens for events then re-triggers them on its own\r
+             * event emitter. Why? So we can .off() on this emitter without\r
+             * effecting the source of events. Handy for plugins that we don't\r
+             * trust meddling with the core events.\r
+             *\r
+             * If listening for 'all' events the arguments are as follows:\r
+             *     1. Name of the triggered event\r
+             *     2. The event data\r
+             * For all other events, we only have one argument:\r
+             *     1. The event data\r
+             *\r
+             * When this is used via `new kiwi.components.Network()`, this listens\r
+             * for 'all' events so the first argument is the event name which is\r
+             * the connection ID. We don't want to re-trigger this event name so\r
+             * we need to juggle the arguments to find the real event name we want\r
+             * to emit.\r
+             */\r
+            function proxyEvent(event_name, event_data) {\r
+                if (proxy_event_name == 'all') {\r
+                } else {\r
+                    event_data = event_name.event_data;\r
+                    event_name = event_name.event_name;\r
+                }\r
+\r
+                this.trigger(event_name, event_data);\r
+            }\r
+\r
+            // The event we are to proxy\r
+            proxy_event_name = proxy_event_name || 'all';\r
+\r
+            _.extend(this, Backbone.Events);\r
+            this._source = event_source;\r
+\r
+            // Proxy the events to this dispatcher\r
+            event_source.on(proxy_event_name, proxyEvent, this);\r
+\r
+            // Clean up this object\r
+            this.dispose = function () {\r
+                event_source.off(proxy_event_name, proxyEvent);\r
+                this.off();\r
+                delete this.event_source;\r
+            };\r
+        },\r
+\r
+        Network: function(connection_id) {\r
+            var connection_event;\r
+\r
+            // If no connection id given, use all connections\r
+            if (typeof connection_id !== 'undefined') {\r
+                connection_event = 'connection:' + connection_id.toString();\r
+            } else {\r
+                connection_event = 'connection';\r
+            }\r
+\r
+            // Helper to get the network object\r
+            var getNetwork = function() {\r
+                var network = typeof connection_id === 'undefined' ?\r
+                    _kiwi.app.connections.active_connection :\r
+                    _kiwi.app.connections.getByConnectionId(connection_id);\r
+\r
+                return network ?\r
+                    network :\r
+                    undefined;\r
+            };\r
+\r
+            // Create the return object (events proxy from the gateway)\r
+            var obj = new this.EventComponent(_kiwi.gateway, connection_event);\r
+\r
+            // Proxy several gateway functions onto the return object\r
+            var funcs = {\r
+                kiwi: 'kiwi', raw: 'raw', kick: 'kick', topic: 'topic',\r
+                part: 'part', join: 'join', action: 'action', ctcp: 'ctcp',\r
+                ctcpRequest: 'ctcpRequest', ctcpResponse: 'ctcpResponse',\r
+                notice: 'notice', msg: 'privmsg', say: 'privmsg',\r
+                changeNick: 'changeNick', channelInfo: 'channelInfo',\r
+                mode: 'mode', quit: 'quit'\r
+            };\r
+\r
+            _.each(funcs, function(gateway_fn, func_name) {\r
+                obj[func_name] = function() {\r
+                    var fn_name = gateway_fn;\r
+\r
+                    // Add connection_id to the argument list\r
+                    var args = Array.prototype.slice.call(arguments, 0);\r
+                    args.unshift(connection_id);\r
+\r
+                    // Call the gateway function on behalf of this connection\r
+                    return _kiwi.gateway[fn_name].apply(_kiwi.gateway, args);\r
+                };\r
+            });\r
+\r
+            // Now for some network related functions...\r
+            obj.createQuery = function(nick) {\r
+                var network, restricted_keys;\r
+\r
+                network = getNetwork();\r
+                if (!network) {\r
+                    return;\r
+                }\r
+\r
+                return network.createQuery(nick);\r
+            };\r
+\r
+            // Add the networks getters/setters\r
+            obj.get = function(name) {\r
+                var network, restricted_keys;\r
+\r
+                network = getNetwork();\r
+                if (!network) {\r
+                    return;\r
+                }\r
+\r
+                restricted_keys = [\r
+                    'password'\r
+                ];\r
+                if (restricted_keys.indexOf(name) > -1) {\r
+                    return undefined;\r
+                }\r
+\r
+                return network.get(name);\r
+            };\r
+\r
+            obj.set = function() {\r
+                var network = getNetwork();\r
+                if (!network) {\r
+                    return;\r
+                }\r
+\r
+                return network.set.apply(network, arguments);\r
+            };\r
+\r
+            return obj;\r
+        },\r
+\r
+        ControlInput: function() {\r
+            var obj = new this.EventComponent(_kiwi.app.controlbox);\r
+            var funcs = {\r
+                run: 'processInput', addPluginIcon: 'addPluginIcon'\r
+            };\r
+\r
+            _.each(funcs, function(controlbox_fn, func_name) {\r
+                obj[func_name] = function() {\r
+                    var fn_name = controlbox_fn;\r
+                    return _kiwi.app.controlbox[fn_name].apply(_kiwi.app.controlbox, arguments);\r
+                };\r
+            });\r
+\r
+            // Give access to the control input textarea\r
+            obj.input = _kiwi.app.controlbox.$('.inp');\r
+\r
+            return obj;\r
+        }\r
+    },\r
+\r
+    // Entry point to start the kiwi application\r
+    init: function (opts, callback) {\r
+        var locale_promise, theme_promise,\r
+            that = this;\r
+\r
+        opts = opts || {};\r
+\r
+        this.initUtils();\r
+\r
+        // Set up the settings datastore\r
+        _kiwi.global.settings = _kiwi.model.DataStore.instance('kiwi.settings');\r
+        _kiwi.global.settings.load();\r
+\r
+        // Set the window title\r
+        window.document.title = opts.server_settings.client.window_title || 'Kiwi IRC';\r
+\r
+        locale_promise = new Promise(function (resolve) {\r
+            var locale = _kiwi.global.settings.get('locale') || 'magic';\r
+            $.getJSON(opts.base_path + '/assets/locales/' + locale + '.json', function (locale) {\r
+                if (locale) {\r
+                    that.i18n = new Jed(locale);\r
+                } else {\r
+                    that.i18n = new Jed();\r
+                }\r
+                resolve();\r
+            });\r
+        });\r
+\r
+        theme_promise = new Promise(function (resolve) {\r
+            var text_theme = opts.server_settings.client.settings.text_theme || 'default';\r
+            $.getJSON(opts.base_path + '/assets/text_themes/' + text_theme + '.json', function(text_theme) {\r
+                opts.text_theme = text_theme;\r
+                resolve();\r
+            });\r
+        });\r
+\r
+\r
+        Promise.all([locale_promise, theme_promise]).then(function () {\r
+            _kiwi.app = new _kiwi.model.Application(opts);\r
+\r
+            // Start the client up\r
+            _kiwi.app.initializeInterfaces();\r
+\r
+            // Event emitter to let plugins interface with parts of kiwi\r
+            _kiwi.global.events  = new PluginInterface();\r
+\r
+            // Now everything has started up, load the plugin manager for third party plugins\r
+            _kiwi.global.plugins = new _kiwi.model.PluginManager();\r
+\r
+            callback();\r
+\r
+        }).then(null, function(err) {\r
+            console.error(err.stack);\r
+        });\r
+    },\r
+\r
+    start: function() {\r
+        _kiwi.app.showStartup();\r
+    },\r
+\r
+    // Allow plugins to change the startup applet\r
+    registerStartupApplet: function(startup_applet_name) {\r
+        _kiwi.app.startup_applet_name = startup_applet_name;\r
+    },\r
+\r
+    /**\r
+     * Open a new IRC connection\r
+     * @param {Object} connection_details {nick, host, port, ssl, password, options}\r
+     * @param {Function} callback function(err, network){}\r
+     */\r
+    newIrcConnection: function(connection_details, callback) {\r
+        _kiwi.gateway.newConnection(connection_details, callback);\r
+    },\r
+\r
+\r
+    /**\r
+     * Taking settings from the server and URL, extract the default server/channel/nick settings\r
+     */\r
+    defaultServerSettings: function () {\r
+        var parts;\r
+        var defaults = {\r
+            nick: '',\r
+            server: '',\r
+            port: 6667,\r
+            ssl: false,\r
+            channel: '',\r
+            channel_key: ''\r
+        };\r
+        var uricheck;\r
+\r
+\r
+        /**\r
+         * Get any settings set by the server\r
+         * These settings may be changed in the server selection dialog or via URL parameters\r
+         */\r
+        if (_kiwi.app.server_settings.client) {\r
+            if (_kiwi.app.server_settings.client.nick)\r
+                defaults.nick = _kiwi.app.server_settings.client.nick;\r
+\r
+            if (_kiwi.app.server_settings.client.server)\r
+                defaults.server = _kiwi.app.server_settings.client.server;\r
+\r
+            if (_kiwi.app.server_settings.client.port)\r
+                defaults.port = _kiwi.app.server_settings.client.port;\r
+\r
+            if (_kiwi.app.server_settings.client.ssl)\r
+                defaults.ssl = _kiwi.app.server_settings.client.ssl;\r
+\r
+            if (_kiwi.app.server_settings.client.channel)\r
+                defaults.channel = _kiwi.app.server_settings.client.channel;\r
+\r
+            if (_kiwi.app.server_settings.client.channel_key)\r
+                defaults.channel_key = _kiwi.app.server_settings.client.channel_key;\r
+        }\r
+\r
+\r
+\r
+        /**\r
+         * Get any settings passed in the URL\r
+         * These settings may be changed in the server selection dialog\r
+         */\r
+\r
+        // Any query parameters first\r
+        if (getQueryVariable('nick'))\r
+            defaults.nick = getQueryVariable('nick');\r
+\r
+        if (window.location.hash)\r
+            defaults.channel = window.location.hash;\r
+\r
+\r
+        // Process the URL part by part, extracting as we go\r
+        parts = window.location.pathname.toString().replace(_kiwi.app.get('base_path'), '').split('/');\r
+\r
+        if (parts.length > 0) {\r
+            parts.shift();\r
+\r
+            if (parts.length > 0 && parts[0]) {\r
+                // Check to see if we're dealing with an irc: uri, or whether we need to extract the server/channel info from the HTTP URL path.\r
+                uricheck = parts[0].substr(0, 7).toLowerCase();\r
+                if ((uricheck === 'ircs%3a') || (uricheck.substr(0,6) === 'irc%3a')) {\r
+                    parts[0] = decodeURIComponent(parts[0]);\r
+                    // irc[s]://<host>[:<port>]/[<channel>[?<password>]]\r
+                    uricheck = /^irc(s)?:(?:\/\/?)?([^:\/]+)(?::([0-9]+))?(?:(?:\/)([^\?]*)(?:(?:\?)(.*))?)?$/.exec(parts[0]);\r
+                    /*\r
+                        uricheck[1] = ssl (optional)\r
+                        uricheck[2] = host\r
+                        uricheck[3] = port (optional)\r
+                        uricheck[4] = channel (optional)\r
+                        uricheck[5] = channel key (optional, channel must also be set)\r
+                    */\r
+                    if (uricheck) {\r
+                        if (typeof uricheck[1] !== 'undefined') {\r
+                            defaults.ssl = true;\r
+                            if (defaults.port === 6667) {\r
+                                defaults.port = 6697;\r
+                            }\r
+                        }\r
+                        defaults.server = uricheck[2];\r
+                        if (typeof uricheck[3] !== 'undefined') {\r
+                            defaults.port = uricheck[3];\r
+                        }\r
+                        if (typeof uricheck[4] !== 'undefined') {\r
+                            defaults.channel = '#' + uricheck[4];\r
+                            if (typeof uricheck[5] !== 'undefined') {\r
+                                defaults.channel_key = uricheck[5];\r
+                            }\r
+                        }\r
+                    }\r
+                    parts = [];\r
+                } else {\r
+                    // Extract the port+ssl if we find one\r
+                    if (parts[0].search(/:/) > 0) {\r
+                        defaults.port = parts[0].substring(parts[0].search(/:/) + 1);\r
+                        defaults.server = parts[0].substring(0, parts[0].search(/:/));\r
+                        if (defaults.port[0] === '+') {\r
+                            defaults.port = parseInt(defaults.port.substring(1), 10);\r
+                            defaults.ssl = true;\r
+                        } else {\r
+                            defaults.ssl = false;\r
+                        }\r
+\r
+                    } else {\r
+                        defaults.server = parts[0];\r
+                    }\r
+\r
+                    parts.shift();\r
+                }\r
+            }\r
+\r
+            if (parts.length > 0 && parts[0]) {\r
+                defaults.channel = '#' + parts[0];\r
+                parts.shift();\r
+            }\r
+        }\r
+\r
+        // If any settings have been given by the server.. override any auto detected settings\r
+        /**\r
+         * Get any server restrictions as set in the server config\r
+         * These settings can not be changed in the server selection dialog\r
+         */\r
+        if (_kiwi.app.server_settings && _kiwi.app.server_settings.connection) {\r
+            if (_kiwi.app.server_settings.connection.server) {\r
+                defaults.server = _kiwi.app.server_settings.connection.server;\r
+            }\r
+\r
+            if (_kiwi.app.server_settings.connection.port) {\r
+                defaults.port = _kiwi.app.server_settings.connection.port;\r
+            }\r
+\r
+            if (_kiwi.app.server_settings.connection.ssl) {\r
+                defaults.ssl = _kiwi.app.server_settings.connection.ssl;\r
+            }\r
+\r
+            if (_kiwi.app.server_settings.connection.channel) {\r
+                defaults.channel = _kiwi.app.server_settings.connection.channel;\r
+            }\r
+\r
+            if (_kiwi.app.server_settings.connection.channel_key) {\r
+                defaults.channel_key = _kiwi.app.server_settings.connection.channel_key;\r
+            }\r
+\r
+            if (_kiwi.app.server_settings.connection.nick) {\r
+                defaults.nick = _kiwi.app.server_settings.connection.nick;\r
+            }\r
+        }\r
+\r
+        // Set any random numbers if needed\r
+        defaults.nick = defaults.nick.replace('?', Math.floor(Math.random() * 100000).toString());\r
+\r
+        if (getQueryVariable('encoding'))\r
+            defaults.encoding = getQueryVariable('encoding');\r
+\r
+        return defaults;\r
+    },\r
+};\r
+\r
+\r
+\r
+// If within a closure, expose the kiwi globals\r
+if (typeof global !== 'undefined') {\r
+    global.kiwi = _kiwi.global;\r
+} else {\r
+    // Not within a closure so set a var in the current scope\r
+    var kiwi = _kiwi.global;\r
+}\r
+
+
+
+(function () {\r
+\r
+    _kiwi.model.Application = Backbone.Model.extend({\r
+        /** _kiwi.view.Application */\r
+        view: null,\r
+\r
+        /** _kiwi.view.StatusMessage */\r
+        message: null,\r
+\r
+        initialize: function (options) {\r
+            this.app_options = options;\r
+\r
+            if (options.container) {\r
+                this.set('container', options.container);\r
+            }\r
+\r
+            // The base url to the kiwi server\r
+            this.set('base_path', options.base_path ? options.base_path : '');\r
+\r
+            // Path for the settings.json file\r
+            this.set('settings_path', options.settings_path ?\r
+                    options.settings_path :\r
+                    this.get('base_path') + '/assets/settings.json'\r
+            );\r
+\r
+            // Any options sent down from the server\r
+            this.server_settings = options.server_settings || {};\r
+            this.translations = options.translations || {};\r
+            this.themes = options.themes || [];\r
+            this.text_theme = options.text_theme || {};\r
+\r
+            // The applet to initially load\r
+            this.startup_applet_name = options.startup || 'kiwi_startup';\r
+\r
+            // Set any default settings before anything else is applied\r
+            if (this.server_settings && this.server_settings.client && this.server_settings.client.settings) {\r
+                this.applyDefaultClientSettings(this.server_settings.client.settings);\r
+            }\r
+        },\r
+\r
+\r
+        initializeInterfaces: function () {\r
+            // Best guess at where the kiwi server is if not already specified\r
+            var kiwi_server = this.app_options.kiwi_server || this.detectKiwiServer();\r
+\r
+            // Set the gateway up\r
+            _kiwi.gateway = new _kiwi.model.Gateway({kiwi_server: kiwi_server});\r
+            this.bindGatewayCommands(_kiwi.gateway);\r
+\r
+            this.initializeClient();\r
+            this.initializeGlobals();\r
+\r
+            this.view.barsHide(true);\r
+        },\r
+\r
+\r
+        detectKiwiServer: function () {\r
+            // If running from file, default to localhost:7777 by default\r
+            if (window.location.protocol === 'file:') {\r
+                return 'http://localhost:7778';\r
+            } else {\r
+                // Assume the kiwi server is on the same server\r
+                return window.location.protocol + '//' + window.location.host;\r
+            }\r
+        },\r
+\r
+\r
+        showStartup: function() {\r
+            this.startup_applet = _kiwi.model.Applet.load(this.startup_applet_name, {no_tab: true});\r
+            this.startup_applet.tab = this.view.$('.console');\r
+            this.startup_applet.view.show();\r
+\r
+            _kiwi.global.events.emit('loaded');\r
+        },\r
+\r
+\r
+        initializeClient: function () {\r
+            this.view = new _kiwi.view.Application({model: this, el: this.get('container')});\r
+\r
+            // Takes instances of model_network\r
+            this.connections = new _kiwi.model.NetworkPanelList();\r
+\r
+            // If all connections are removed at some point, hide the bars\r
+            this.connections.on('remove', _.bind(function() {\r
+                if (this.connections.length === 0) {\r
+                    this.view.barsHide();\r
+                }\r
+            }, this));\r
+\r
+            // Applets panel list\r
+            this.applet_panels = new _kiwi.model.PanelList();\r
+            this.applet_panels.view.$el.addClass('panellist applets');\r
+            this.view.$el.find('.tabs').append(this.applet_panels.view.$el);\r
+\r
+            /**\r
+             * Set the UI components up\r
+             */\r
+            this.controlbox = (new _kiwi.view.ControlBox({el: $('#kiwi .controlbox')[0]})).render();\r
+            this.client_ui_commands = new _kiwi.misc.ClientUiCommands(this, this.controlbox);\r
+\r
+            this.rightbar = new _kiwi.view.RightBar({el: this.view.$('.right_bar')[0]});\r
+            this.topicbar = new _kiwi.view.TopicBar({el: this.view.$el.find('.topic')[0]});\r
+\r
+            new _kiwi.view.AppToolbar({el: _kiwi.app.view.$el.find('.toolbar .app_tools')[0]});\r
+            new _kiwi.view.ChannelTools({el: _kiwi.app.view.$el.find('.channel_tools')[0]});\r
+\r
+            this.message = new _kiwi.view.StatusMessage({el: this.view.$el.find('.status_message')[0]});\r
+\r
+            this.resize_handle = new _kiwi.view.ResizeHandler({el: this.view.$el.find('.memberlists_resize_handle')[0]});\r
+\r
+            // Rejigg the UI sizes\r
+            this.view.doLayout();\r
+        },\r
+\r
+\r
+        initializeGlobals: function () {\r
+            _kiwi.global.connections = this.connections;\r
+\r
+            _kiwi.global.panels = this.panels;\r
+            _kiwi.global.panels.applets = this.applet_panels;\r
+\r
+            _kiwi.global.components.Applet = _kiwi.model.Applet;\r
+            _kiwi.global.components.Panel =_kiwi.model.Panel;\r
+            _kiwi.global.components.MenuBox = _kiwi.view.MenuBox;\r
+            _kiwi.global.components.DataStore = _kiwi.model.DataStore;\r
+            _kiwi.global.components.Notification = _kiwi.view.Notification;\r
+            _kiwi.global.components.Events = function() {\r
+                return kiwi.events.createProxy();\r
+            };\r
+        },\r
+\r
+\r
+        applyDefaultClientSettings: function (settings) {\r
+            _.each(settings, function (value, setting) {\r
+                if (typeof _kiwi.global.settings.get(setting) === 'undefined') {\r
+                    _kiwi.global.settings.set(setting, value);\r
+                }\r
+            });\r
+        },\r
+\r
+\r
+        panels: (function() {\r
+            var active_panel;\r
+\r
+            var fn = function(panel_type) {\r
+                var app = _kiwi.app,\r
+                    panels;\r
+\r
+                // Default panel type\r
+                panel_type = panel_type || 'connections';\r
+\r
+                switch (panel_type) {\r
+                case 'connections':\r
+                    panels = app.connections.panels();\r
+                    break;\r
+                case 'applets':\r
+                    panels = app.applet_panels.models;\r
+                    break;\r
+                }\r
+\r
+                // Active panels / server\r
+                panels.active = active_panel;\r
+                panels.server = app.connections.active_connection ?\r
+                    app.connections.active_connection.panels.server :\r
+                    null;\r
+\r
+                return panels;\r
+            };\r
+\r
+            _.extend(fn, Backbone.Events);\r
+\r
+            // Keep track of the active panel. Channel/query/server or applet\r
+            fn.bind('active', function (new_active_panel) {\r
+                var previous_panel = active_panel;\r
+                active_panel = new_active_panel;\r
+\r
+                _kiwi.global.events.emit('panel:active', {previous: previous_panel, active: active_panel});\r
+            });\r
+\r
+            return fn;\r
+        })(),\r
+\r
+\r
+        bindGatewayCommands: function (gw) {\r
+            var that = this;\r
+\r
+            // As soon as an IRC connection is made, show the full client UI\r
+            gw.on('connection:connect', function (event) {\r
+                that.view.barsShow();\r
+            });\r
+\r
+\r
+            /**\r
+             * Handle the reconnections to the kiwi server\r
+             */\r
+            (function () {\r
+                // 0 = non-reconnecting state. 1 = reconnecting state.\r
+                var gw_stat = 0;\r
+\r
+                gw.on('disconnect', function (event) {\r
+                    that.view.$el.removeClass('connected');\r
+\r
+                    // Reconnection phase will start to kick in\r
+                    gw_stat = 1;\r
+                });\r
+\r
+\r
+                gw.on('reconnecting', function (event) {\r
+                    var msg = translateText('client_models_application_reconnect_in_x_seconds', [event.delay/1000]) + '...';\r
+\r
+                    // Only need to mention the repeating re-connection messages on server panels\r
+                    _kiwi.app.connections.forEach(function(connection) {\r
+                        connection.panels.server.addMsg('', styleText('quit', {text: msg}), 'action quit');\r
+                    });\r
+                });\r
+\r
+\r
+                // After the socket has connected, kiwi handshakes and then triggers a kiwi:connected event\r
+                gw.on('kiwi:connected', function (event) {\r
+                    var msg;\r
+\r
+                    that.view.$el.addClass('connected');\r
+\r
+                    // Make the rpc globally available for plugins\r
+                    _kiwi.global.rpc = _kiwi.gateway.rpc;\r
+\r
+                    _kiwi.global.events.emit('connected');\r
+\r
+                    // If we were reconnecting, show some messages we have connected back OK\r
+                    if (gw_stat === 1) {\r
+\r
+                        // No longer in the reconnection state\r
+                        gw_stat = 0;\r
+\r
+                        msg = translateText('client_models_application_reconnect_successfully') + ' :)';\r
+                        that.message.text(msg, {timeout: 5000});\r
+\r
+                        // Mention the re-connection on every channel\r
+                        _kiwi.app.connections.forEach(function(connection) {\r
+                            connection.reconnect();\r
+\r
+                            connection.panels.server.addMsg('', styleText('rejoin', {text: msg}), 'action join');\r
+\r
+                            connection.panels.forEach(function(panel) {\r
+                                if (!panel.isChannel())\r
+                                    return;\r
+\r
+                                panel.addMsg('', styleText('rejoin', {text: msg}), 'action join');\r
+                            });\r
+                        });\r
+                    }\r
+\r
+                });\r
+            })();\r
+\r
+\r
+            gw.on('kiwi:reconfig', function () {\r
+                $.getJSON(that.get('settings_path'), function (data) {\r
+                    that.server_settings = data.server_settings || {};\r
+                    that.translations = data.translations || {};\r
+                });\r
+            });\r
+\r
+\r
+            gw.on('kiwi:jumpserver', function (data) {\r
+                var serv;\r
+                // No server set? Then nowhere to jump to.\r
+                if (typeof data.kiwi_server === 'undefined')\r
+                    return;\r
+\r
+                serv = data.kiwi_server;\r
+\r
+                // Strip any trailing slash from the end\r
+                if (serv[serv.length-1] === '/')\r
+                    serv = serv.substring(0, serv.length-1);\r
+\r
+                // Force the jumpserver now?\r
+                if (data.force) {\r
+                    // Get an interval between 5 and 6 minutes so everyone doesn't reconnect it all at once\r
+                    var jump_server_interval = Math.random() * (360 - 300) + 300;\r
+                    jump_server_interval = 1;\r
+\r
+                    // Tell the user we are going to disconnect, wait 5 minutes then do the actual reconnect\r
+                    var msg = _kiwi.global.i18n.translate('client_models_application_jumpserver_prepare').fetch();\r
+                    that.message.text(msg, {timeout: 10000});\r
+\r
+                    setTimeout(function forcedReconnect() {\r
+                        var msg = _kiwi.global.i18n.translate('client_models_application_jumpserver_reconnect').fetch();\r
+                        that.message.text(msg, {timeout: 8000});\r
+\r
+                        setTimeout(function forcedReconnectPartTwo() {\r
+                            _kiwi.gateway.set('kiwi_server', serv);\r
+\r
+                            _kiwi.gateway.reconnect(function() {\r
+                                // Reconnect all the IRC connections\r
+                                that.connections.forEach(function(con){ con.reconnect(); });\r
+                            });\r
+                        }, 5000);\r
+\r
+                    }, jump_server_interval * 1000);\r
+                }\r
+            });\r
+        }\r
+\r
+    });\r
+\r
+})();\r
+
+
+
+_kiwi.model.Gateway = Backbone.Model.extend({\r
+\r
+    initialize: function () {\r
+\r
+        // For ease of access. The socket.io object\r
+        this.socket = this.get('socket');\r
+\r
+        // Used to check if a disconnection was unplanned\r
+        this.disconnect_requested = false;\r
+    },\r
+\r
+\r
+\r
+    reconnect: function (callback) {\r
+        this.disconnect_requested = true;\r
+        this.socket.close();\r
+\r
+        this.socket = null;\r
+        this.connect(callback);\r
+    },\r
+\r
+\r
+\r
+    /**\r
+    *   Connects to the server\r
+    *   @param  {Function}  callback    A callback function to be invoked once Kiwi's server has connected to the IRC server\r
+    */\r
+    connect: function (callback) {\r
+        var that = this;\r
+\r
+        this.connect_callback = callback;\r
+\r
+        this.socket = new EngineioTools.ReconnectingSocket(this.get('kiwi_server'), {\r
+            transports: _kiwi.app.server_settings.transports || ['polling', 'websocket'],\r
+            path: _kiwi.app.get('base_path') + '/transport',\r
+            reconnect_max_attempts: 5,\r
+            reconnect_delay: 2000\r
+        });\r
+\r
+        // If we have an existing RPC object, clean it up before replacing it\r
+        if (this.rpc) {\r
+            rpc.dispose();\r
+        }\r
+        this.rpc = new EngineioTools.Rpc(this.socket);\r
+\r
+        this.socket.on('connect_failed', function (reason) {\r
+            this.socket.disconnect();\r
+            this.trigger("connect_fail", {reason: reason});\r
+        });\r
+\r
+        this.socket.on('error', function (e) {\r
+            console.log("_kiwi.gateway.socket.on('error')", {reason: e});\r
+            if (that.connect_callback) {\r
+                that.connect_callback(e);\r
+                delete that.connect_callback;\r
+            }\r
+\r
+            that.trigger("connect_fail", {reason: e});\r
+        });\r
+\r
+        this.socket.on('connecting', function (transport_type) {\r
+            console.log("_kiwi.gateway.socket.on('connecting')");\r
+            that.trigger("connecting");\r
+        });\r
+\r
+        /**\r
+         * Once connected to the kiwi server send the IRC connect command along\r
+         * with the IRC server details.\r
+         * A `connect` event is sent from the kiwi server once connected to the\r
+         * IRCD and the nick has been accepted.\r
+         */\r
+        this.socket.on('open', function () {\r
+            // Reset the disconnect_requested flag\r
+            that.disconnect_requested = false;\r
+\r
+            // Each minute we need to trigger a heartbeat. Server expects 2min, but to be safe we do it every 1min\r
+            var heartbeat = function() {\r
+                if (!that.rpc) return;\r
+\r
+                that.rpc('kiwi.heartbeat');\r
+                that._heartbeat_tmr = setTimeout(heartbeat, 60000);\r
+            };\r
+\r
+            heartbeat();\r
+\r
+            console.log("_kiwi.gateway.socket.on('open')");\r
+        });\r
+\r
+        this.rpc.on('too_many_connections', function () {\r
+            that.trigger("connect_fail", {reason: 'too_many_connections'});\r
+        });\r
+\r
+        this.rpc.on('irc', function (response, data) {\r
+            that.parse(data.command, data.data);\r
+        });\r
+\r
+        this.rpc.on('kiwi', function (response, data) {\r
+            that.parseKiwi(data.command, data.data);\r
+        });\r
+\r
+        this.socket.on('close', function () {\r
+            that.trigger("disconnect", {});\r
+            console.log("_kiwi.gateway.socket.on('close')");\r
+        });\r
+\r
+        this.socket.on('reconnecting', function (status) {\r
+            console.log("_kiwi.gateway.socket.on('reconnecting')");\r
+            that.trigger("reconnecting", {delay: status.delay, attempts: status.attempts});\r
+        });\r
+\r
+        this.socket.on('reconnecting_failed', function () {\r
+            console.log("_kiwi.gateway.socket.on('reconnect_failed')");\r
+        });\r
+    },\r
+\r
+\r
+    /**\r
+     * Return a new network object with the new connection details\r
+     */\r
+    newConnection: function(connection_info, callback_fn) {\r
+        var that = this;\r
+\r
+        // If not connected, connect first then re-call this function\r
+        if (!this.isConnected()) {\r
+            this.connect(function(err) {\r
+                if (err) {\r
+                    callback_fn(err);\r
+                    return;\r
+                }\r
+\r
+                that.newConnection(connection_info, callback_fn);\r
+            });\r
+\r
+            return;\r
+        }\r
+\r
+        this.makeIrcConnection(connection_info, function(err, server_num) {\r
+            var connection;\r
+\r
+            if (!err) {\r
+                if (!_kiwi.app.connections.getByConnectionId(server_num)){\r
+                    var inf = {\r
+                        connection_id: server_num,\r
+                        nick: connection_info.nick,\r
+                        address: connection_info.host,\r
+                        port: connection_info.port,\r
+                        ssl: connection_info.ssl,\r
+                        password: connection_info.password\r
+                    };\r
+                    connection = new _kiwi.model.Network(inf);\r
+                    _kiwi.app.connections.add(connection);\r
+                }\r
+\r
+                console.log("_kiwi.gateway.socket.on('connect')", connection);\r
+                callback_fn && callback_fn(err, connection);\r
+\r
+            } else {\r
+                console.log("_kiwi.gateway.socket.on('error')", {reason: err});\r
+                callback_fn && callback_fn(err);\r
+            }\r
+        });\r
+    },\r
+\r
+\r
+    /**\r
+     * Make a new IRC connection and return its connection ID\r
+     */\r
+    makeIrcConnection: function(connection_info, callback_fn) {\r
+        var server_info = {\r
+            nick:       connection_info.nick,\r
+            hostname:   connection_info.host,\r
+            port:       connection_info.port,\r
+            ssl:        connection_info.ssl,\r
+            password:   connection_info.password\r
+        };\r
+\r
+        connection_info.options = connection_info.options || {};\r
+\r
+        // A few optional parameters\r
+        if (connection_info.options.encoding)\r
+            server_info.encoding = connection_info.options.encoding;\r
+\r
+        this.rpc('kiwi.connect_irc', server_info, function (err, server_num) {\r
+            if (!err) {\r
+                callback_fn && callback_fn(err, server_num);\r
+\r
+            } else {\r
+                callback_fn && callback_fn(err);\r
+            }\r
+        });\r
+    },\r
+\r
+\r
+    isConnected: function () {\r
+        // TODO: Check this. Might want to use .readyState\r
+        return this.socket;\r
+    },\r
+\r
+\r
+\r
+    parseKiwi: function (command, data) {\r
+        var args;\r
+\r
+        switch (command) {\r
+        case 'connected':\r
+            // Send some info on this client to the server\r
+            args = {\r
+                build_version: _kiwi.global.build_version\r
+            };\r
+            this.rpc('kiwi.client_info', args);\r
+\r
+            this.connect_callback && this.connect_callback();\r
+            delete this.connect_callback;\r
+\r
+            break;\r
+        }\r
+\r
+        this.trigger('kiwi:' + command, data);\r
+        this.trigger('kiwi', data);\r
+    },\r
+\r
+    /**\r
+    *   Parses the response from the server\r
+    */\r
+    parse: function (command, data) {\r
+        var network_trigger = '';\r
+\r
+        // Trigger the connection specific events (used by Network objects)\r
+        if (typeof data.connection_id !== 'undefined') {\r
+            network_trigger = 'connection:' + data.connection_id.toString();\r
+\r
+            this.trigger(network_trigger, {\r
+                event_name: command,\r
+                event_data: data\r
+            });\r
+\r
+            // Some events trigger a more in-depth event name\r
+            if (command == 'message' && data.type) {\r
+                this.trigger('connection ' + network_trigger, {\r
+                    event_name: 'message:' + data.type,\r
+                    event_data: data\r
+                });\r
+            }\r
+\r
+            if (command == 'channel' && data.type) {\r
+                this.trigger('connection ' + network_trigger, {\r
+                    event_name: 'channel:' + data.type,\r
+                    event_data: data\r
+                });\r
+            }\r
+        }\r
+\r
+        // Trigger the global events\r
+        this.trigger('connection', {event_name: command, event_data: data});\r
+        this.trigger('connection:' + command, data);\r
+    },\r
+\r
+    /**\r
+    *   Make an RPC call with the connection_id as the first argument\r
+    *   @param  {String}    method          RPC method name\r
+    *   @param  {Number}    connection_id   Connection ID this call relates to\r
+    */\r
+    rpcCall: function(method, connection_id) {\r
+        var args = Array.prototype.slice.call(arguments, 0);\r
+\r
+        if (typeof args[1] === 'undefined' || args[1] === null)\r
+            args[1] = _kiwi.app.connections.active_connection.get('connection_id');\r
+\r
+        return this.rpc.apply(this.rpc, args);\r
+    },\r
+\r
+    /**\r
+    *   Sends a PRIVMSG message\r
+    *   @param  {String}    target      The target of the message (e.g. a channel or nick)\r
+    *   @param  {String}    msg         The message to send\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    privmsg: function (connection_id, target, msg, callback) {\r
+        var args = {\r
+            target: target,\r
+            msg: msg\r
+        };\r
+\r
+        this.rpcCall('irc.privmsg', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    *   Sends a NOTICE message\r
+    *   @param  {String}    target      The target of the message (e.g. a channel or nick)\r
+    *   @param  {String}    msg         The message to send\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    notice: function (connection_id, target, msg, callback) {\r
+        var args = {\r
+            target: target,\r
+            msg: msg\r
+        };\r
+\r
+        this.rpcCall('irc.notice', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    *   Sends a CTCP message\r
+    *   @param  {Boolean}   request     Indicates whether this is a CTCP request (true) or reply (false)\r
+    *   @param  {String}    type        The type of CTCP message, e.g. 'VERSION', 'TIME', 'PING' etc.\r
+    *   @param  {String}    target      The target of the message, e.g a channel or nick\r
+    *   @param  {String}    params      Additional paramaters\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    ctcp: function (connection_id, is_request, type, target, params, callback) {\r
+        var args = {\r
+            is_request: is_request,\r
+            type: type,\r
+            target: target,\r
+            params: params\r
+        };\r
+\r
+        this.rpcCall('irc.ctcp', connection_id, args, callback);\r
+    },\r
+\r
+    ctcpRequest: function (connection_id, type, target, params, callback) {\r
+        this.ctcp(connection_id, true, type, target, params, callback);\r
+    },\r
+    ctcpResponse: function (connection_id, type, target, params, callback) {\r
+        this.ctcp(connection_id, false, type, target, params, callback);\r
+    },\r
+\r
+    /**\r
+    *   @param  {String}    target      The target of the message (e.g. a channel or nick)\r
+    *   @param  {String}    msg         The message to send\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    action: function (connection_id, target, msg, callback) {\r
+        this.ctcp(connection_id, true, 'ACTION', target, msg, callback);\r
+    },\r
+\r
+    /**\r
+    *   Joins a channel\r
+    *   @param  {String}    channel     The channel to join\r
+    *   @param  {String}    key         The key to the channel\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    join: function (connection_id, channel, key, callback) {\r
+        var args = {\r
+            channel: channel,\r
+            key: key\r
+        };\r
+\r
+        this.rpcCall('irc.join', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    *   Retrieves channel information\r
+    */\r
+    channelInfo: function (connection_id, channel, callback) {\r
+        var args = {\r
+            channel: channel\r
+        };\r
+\r
+        this.rpcCall('irc.channel_info', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    *   Leaves a channel\r
+    *   @param  {String}    channel     The channel to part\r
+    *   @param  {String}    message     Optional part message\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    part: function (connection_id, channel, message, callback) {\r
+        "use strict";\r
+\r
+        // The message param is optional, so juggle args if it is missing\r
+        if (typeof arguments[2] === 'function') {\r
+            callback = arguments[2];\r
+            message = undefined;\r
+        }\r
+        var args = {\r
+            channel: channel,\r
+            message: message\r
+        };\r
+\r
+        this.rpcCall('irc.part', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    *   Queries or modifies a channell topic\r
+    *   @param  {String}    channel     The channel to query or modify\r
+    *   @param  {String}    new_topic   The new topic to set\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    topic: function (connection_id, channel, new_topic, callback) {\r
+        var args = {\r
+            channel: channel,\r
+            topic: new_topic\r
+        };\r
+\r
+        this.rpcCall('irc.topic', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    *   Kicks a user from a channel\r
+    *   @param  {String}    channel     The channel to kick the user from\r
+    *   @param  {String}    nick        The nick of the user to kick\r
+    *   @param  {String}    reason      The reason for kicking the user\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    kick: function (connection_id, channel, nick, reason, callback) {\r
+        var args = {\r
+            channel: channel,\r
+            nick: nick,\r
+            reason: reason\r
+        };\r
+\r
+        this.rpcCall('irc.kick', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    *   Disconnects us from the server\r
+    *   @param  {String}    msg         The quit message to send to the IRC server\r
+    *   @param  {Function}   callback    A callback function\r
+    */\r
+    quit: function (connection_id, msg, callback) {\r
+        msg = msg || "";\r
+\r
+        var args = {\r
+            message: msg\r
+        };\r
+\r
+        this.rpcCall('irc.quit', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    *   Sends a string unmodified to the IRC server\r
+    *   @param  {String}    data        The data to send to the IRC server\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    raw: function (connection_id, data, callback) {\r
+        var args = {\r
+            data: data\r
+        };\r
+\r
+        this.rpcCall('irc.raw', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    *   Changes our nickname\r
+    *   @param  {String}    new_nick    Our new nickname\r
+    *   @param  {Function}  callback    A callback function\r
+    */\r
+    changeNick: function (connection_id, new_nick, callback) {\r
+        var args = {\r
+            nick: new_nick\r
+        };\r
+\r
+        this.rpcCall('irc.nick', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+    * Sets a mode for a target\r
+    */\r
+    mode: function (connection_id, target, mode_string, callback) {\r
+        var args = {\r
+            data: 'MODE ' + target + ' ' + mode_string\r
+        };\r
+\r
+        this.rpcCall('irc.raw', connection_id, args, callback);\r
+    },\r
+\r
+    /**\r
+     *  Sends ENCODING change request to server.\r
+     *  @param  {String}     new_encoding  The new proposed encode\r
+     *  @param  {Fucntion}   callback      A callback function\r
+     */\r
+    setEncoding: function (connection_id, new_encoding, callback) {\r
+        var args = {\r
+            encoding: new_encoding\r
+        };\r
+\r
+        this.rpcCall('irc.encoding', connection_id, args, callback);\r
+    }\r
+});\r
+
+
+
+(function () {
+
+    _kiwi.model.Network = Backbone.Model.extend({
+        defaults: {
+            connection_id: 0,
+            /**
+            *   The name of the network
+            *   @type    String
+            */
+            name: 'Network',
+
+            /**
+            *   The address (URL) of the network
+            *   @type    String
+            */
+            address: '',
+
+            /**
+            *   The port for the network
+            *   @type    Int
+            */
+            port: 6667,
+
+            /**
+            *   If this network uses SSL
+            *   @type    Bool
+            */
+            ssl: false,
+
+            /**
+            *   The password to connect to this network
+            *   @type    String
+            */
+            password: '',
+
+            /**
+            *   The current nickname
+            *   @type   String
+            */
+            nick: '',
+
+            /**
+            *   The channel prefix for this network
+            *   @type    String
+            */
+            channel_prefix: '#',
+
+            /**
+            *   The user prefixes for channel owner/admin/op/voice etc. on this network
+            *   @type   Array
+            */
+            user_prefixes: [
+                {symbol: '~', mode: 'q'},
+                {symbol: '&', mode: 'a'},
+                {symbol: '@', mode: 'o'},
+                {symbol: '%', mode: 'h'},
+                {symbol: '+', mode: 'v'}
+            ],
+
+            /**
+            *   List of nicks we are ignoring
+            *   @type Array
+            */
+            ignore_list: []
+        },
+
+
+        initialize: function () {
+            // If we already have a connection, bind our events
+            if (typeof this.get('connection_id') !== 'undefined') {
+                this.gateway = _kiwi.global.components.Network(this.get('connection_id'));
+                this.bindGatewayEvents();
+            }
+
+            // Create our panel list (tabs)
+            this.panels = new _kiwi.model.PanelList([], this);
+            //this.panels.network = this;
+
+            // Automatically create a server tab
+            var server_panel = new _kiwi.model.Server({name: 'Server', network: this});
+            this.panels.add(server_panel);
+            this.panels.server = this.panels.active = server_panel;
+        },
+
+
+        reconnect: function(callback_fn) {
+            var that = this,
+                server_info = {
+                    nick:       this.get('nick'),
+                    host:       this.get('address'),
+                    port:       this.get('port'),
+                    ssl:        this.get('ssl'),
+                    password:   this.get('password')
+                };
+
+            _kiwi.gateway.makeIrcConnection(server_info, function(err, connection_id) {
+                if (!err) {
+                    that.gateway.dispose();
+
+                    that.set('connection_id', connection_id);
+                    that.gateway = _kiwi.global.components.Network(that.get('connection_id'));
+                    that.bindGatewayEvents();
+
+                    // Reset each of the panels connection ID
+                    that.panels.forEach(function(panel) {
+                        panel.set('connection_id', connection_id);
+                    });
+
+                    callback_fn && callback_fn(err);
+
+                } else {
+                    console.log("_kiwi.gateway.socket.on('error')", {reason: err});
+                    callback_fn && callback_fn(err);
+                }
+            });
+        },
+
+
+        bindGatewayEvents: function () {
+            //this.gateway.on('all', function() {console.log('ALL', this.get('connection_id'), arguments);});
+
+            this.gateway.on('connect', onConnect, this);
+            this.gateway.on('disconnect', onDisconnect, this);
+
+            this.gateway.on('nick', function(event) {
+                if (event.nick === this.get('nick')) {
+                    this.set('nick', event.newnick);
+                }
+            }, this);
+
+            this.gateway.on('options', onOptions, this);
+            this.gateway.on('motd', onMotd, this);
+            this.gateway.on('channel:join', onJoin, this);
+            this.gateway.on('channel:part', onPart, this);
+            this.gateway.on('channel:kick', onKick, this);
+            this.gateway.on('quit', onQuit, this);
+            this.gateway.on('message', onMessage, this);
+            this.gateway.on('nick', onNick, this);
+            this.gateway.on('ctcp_request', onCtcpRequest, this);
+            this.gateway.on('ctcp_response', onCtcpResponse, this);
+            this.gateway.on('topic', onTopic, this);
+            this.gateway.on('topicsetby', onTopicSetBy, this);
+            this.gateway.on('userlist', onUserlist, this);
+            this.gateway.on('userlist_end', onUserlistEnd, this);
+            this.gateway.on('banlist', onBanlist, this);
+            this.gateway.on('mode', onMode, this);
+            this.gateway.on('whois', onWhois, this);
+            this.gateway.on('whowas', onWhowas, this);
+            this.gateway.on('away', onAway, this);
+            this.gateway.on('list_start', onListStart, this);
+            this.gateway.on('irc_error', onIrcError, this);
+            this.gateway.on('unknown_command', onUnknownCommand, this);
+            this.gateway.on('channel_info', onChannelInfo, this);
+            this.gateway.on('wallops', onWallops, this);
+        },
+
+
+        /**
+         * Create panels and join the channel
+         * This will not wait for the join event to create a panel. This
+         * increases responsiveness in case of network lag
+         */
+        createAndJoinChannels: function (channels) {
+            var that = this,
+                panels = [];
+
+            // Multiple channels may come as comma-delimited
+            if (typeof channels === 'string') {
+                channels = channels.split(',');
+            }
+
+            $.each(channels, function (index, channel_name_key) {
+                // We may have a channel key so split it off
+                var spli = channel_name_key.trim().split(' '),
+                    channel_name = spli[0],
+                    channel_key = spli[1] || '';
+
+                // Trim any whitespace off the name
+                channel_name = channel_name.trim();
+
+                // Add channel_prefix in front of the first channel if missing
+                if (that.get('channel_prefix').indexOf(channel_name[0]) === -1) {
+                    // Could be many prefixes but '#' is highly likely the required one
+                    channel_name = '#' + channel_name;
+                }
+
+                // Check if we have the panel already. If not, create it
+                channel = that.panels.getByName(channel_name);
+                if (!channel) {
+                    channel = new _kiwi.model.Channel({name: channel_name, network: that});
+                    that.panels.add(channel);
+                }
+
+                panels.push(channel);
+
+                that.gateway.join(channel_name, channel_key);
+            });
+
+
+            return panels;
+        },
+
+
+        /**
+         * Join all the open channels we have open
+         * Reconnecting to a network would typically call this.
+         */
+        rejoinAllChannels: function() {
+            var that = this;
+
+            this.panels.forEach(function(panel) {
+                if (!panel.isChannel())
+                    return;
+
+                that.gateway.join(panel.get('name'));
+            });
+        },
+
+        isChannelName: function (channel_name) {
+            var channel_prefix = this.get('channel_prefix');
+
+            if (!channel_name || !channel_name.length) return false;
+            return (channel_prefix.indexOf(channel_name[0]) > -1);
+        },
+
+        // Check a nick alongside our ignore list
+        isNickIgnored: function (nick) {
+            var idx, list = this.get('ignore_list');
+            var pattern, regex;
+
+            for (idx = 0; idx < list.length; idx++) {
+                pattern = list[idx].replace(/([.+^$[\]\\(){}|-])/g, "\\$1")
+                    .replace('*', '.*')
+                    .replace('?', '.');
+
+                regex = new RegExp(pattern, 'i');
+                if (regex.test(nick)) return true;
+            }
+
+            return false;
+        },
+
+        // Create a new query panel
+        createQuery: function (nick) {
+            var that = this,
+                query;
+
+            // Check if we have the panel already. If not, create it
+            query = that.panels.getByName(nick);
+            if (!query) {
+                query = new _kiwi.model.Query({name: nick});
+                that.panels.add(query);
+            }
+
+            // In all cases, show the demanded query
+            query.view.show();
+
+            return query;
+        }
+    });
+
+
+
+    function onDisconnect(event) {
+        this.set('connected', false);
+
+        $.each(this.panels.models, function (index, panel) {
+            if (!panel.isApplet()) {
+                panel.addMsg('', styleText('network_disconnected', {text: translateText('client_models_network_disconnected', [])}), 'action quit');
+            }
+        });
+    }
+
+
+
+    function onConnect(event) {
+        var panels, channel_names;
+
+        // Update our nick with what the network gave us
+        this.set('nick', event.nick);
+
+        this.set('connected', true);
+
+        // If this is a re-connection then we may have some channels to re-join
+        this.rejoinAllChannels();
+
+        // Auto joining channels
+        if (this.auto_join && this.auto_join.channel) {
+            panels = this.createAndJoinChannels(this.auto_join.channel + ' ' + (this.auto_join.key || ''));
+
+            // Show the last channel if we have one
+            if (panels)
+                panels[panels.length - 1].view.show();
+
+            delete this.auto_join;
+        }
+    }
+
+
+
+    function onOptions(event) {
+        var that = this;
+
+        $.each(event.options, function (name, value) {
+            switch (name) {
+            case 'CHANTYPES':
+                that.set('channel_prefix', value.join(''));
+                break;
+            case 'NETWORK':
+                that.set('name', value);
+                break;
+            case 'PREFIX':
+                that.set('user_prefixes', value);
+                break;
+            }
+        });
+
+        this.set('cap', event.cap);
+    }
+
+
+
+    function onMotd(event) {
+        this.panels.server.addMsg(this.get('name'), styleText('motd', {text: event.msg}), 'motd');
+    }
+
+
+
+    function onJoin(event) {
+        var c, members, user;
+        c = this.panels.getByName(event.channel);
+        if (!c) {
+            c = new _kiwi.model.Channel({name: event.channel, network: this});
+            this.panels.add(c);
+        }
+
+        members = c.get('members');
+        if (!members) return;
+
+        // Do we already have this member?
+        if (members.getByNick(event.nick)) {
+            return;
+        }
+
+        user = new _kiwi.model.Member({
+            nick: event.nick,
+            ident: event.ident,
+            hostname: event.hostname,
+            user_prefixes: this.get('user_prefixes')
+        });
+
+        _kiwi.global.events.emit('channel:join', {channel: event.channel, user: user, network: this.gateway})
+        .then(function() {
+            members.add(user, {kiwi: event});
+        });
+    }
+
+
+
+    function onPart(event) {
+        var channel, members, user,
+            part_options = {};
+
+        part_options.type = 'part';
+        part_options.message = event.message || '';
+        part_options.time = event.time;
+
+        channel = this.panels.getByName(event.channel);
+        if (!channel) return;
+
+        // If this is us, close the panel
+        if (event.nick === this.get('nick')) {
+            channel.close();
+            return;
+        }
+
+        members = channel.get('members');
+        if (!members) return;
+
+        user = members.getByNick(event.nick);
+        if (!user) return;
+
+        _kiwi.global.events.emit('channel:leave', {channel: event.channel, user: user, type: 'part', message: part_options.message, network: this.gateway})
+        .then(function() {
+            members.remove(user, {kiwi: part_options});
+        });
+    }
+
+
+
+    function onQuit(event) {
+        var member, members,
+            quit_options = {};
+
+        quit_options.type = 'quit';
+        quit_options.message = event.message || '';
+        quit_options.time = event.time;
+
+        $.each(this.panels.models, function (index, panel) {
+            // Let any query panels know they quit
+            if (panel.isQuery() && panel.get('name').toLowerCase() === event.nick.toLowerCase()) {
+                panel.addMsg(' ', styleText('channel_quit', {
+                    nick: event.nick,
+                    text: translateText('client_models_channel_quit', [quit_options.message])
+                }), 'action quit', {time: quit_options.time});
+            }
+
+            // Remove the nick from any channels
+            if (panel.isChannel()) {
+                member = panel.get('members').getByNick(event.nick);
+                if (member) {
+                    _kiwi.global.events.emit('channel:leave', {channel: panel.get('name'), user: member, type: 'quit', message: quit_options.message, network: this.gateway})
+                    .then(function() {
+                        panel.get('members').remove(member, {kiwi: quit_options});
+                    });
+                }
+            }
+        });
+    }
+
+
+
+    function onKick(event) {
+        var channel, members, user,
+            part_options = {};
+
+        part_options.type = 'kick';
+        part_options.by = event.nick;
+        part_options.message = event.message || '';
+        part_options.current_user_kicked = (event.kicked == this.get('nick'));
+        part_options.current_user_initiated = (event.nick == this.get('nick'));
+        part_options.time = event.time;
+
+        channel = this.panels.getByName(event.channel);
+        if (!channel) return;
+
+        members = channel.get('members');
+        if (!members) return;
+
+        user = members.getByNick(event.kicked);
+        if (!user) return;
+
+
+        _kiwi.global.events.emit('channel:leave', {channel: event.channel, user: user, type: 'kick', message: part_options.message, network: this.gateway})
+        .then(function() {
+            members.remove(user, {kiwi: part_options});
+
+            if (part_options.current_user_kicked) {
+                members.reset([]);
+            }
+        });
+    }
+
+
+
+    function onMessage(event) {
+        _kiwi.global.events.emit('message:new', {network: this.gateway, message: event})
+        .then(_.bind(function() {
+            var panel,
+                is_pm = ((event.target || '').toLowerCase() == this.get('nick').toLowerCase());
+
+            // An ignored user? don't do anything with it
+            if (this.isNickIgnored(event.nick)) {
+                return;
+            }
+
+            if (event.type == 'notice') {
+                if (event.from_server) {
+                    panel = this.panels.server;
+
+                } else {
+                    panel = this.panels.getByName(event.target) || this.panels.getByName(event.nick);
+
+                    // Forward ChanServ messages to its associated channel
+                    if (event.nick && event.nick.toLowerCase() == 'chanserv' && event.msg.charAt(0) == '[') {
+                        channel_name = /\[([^ \]]+)\]/gi.exec(event.msg);
+                        if (channel_name && channel_name[1]) {
+                            channel_name = channel_name[1];
+
+                            panel = this.panels.getByName(channel_name);
+                        }
+                    }
+
+                }
+
+                if (!panel) {
+                    panel = this.panels.server;
+                }
+
+            } else if (is_pm) {
+                // If a panel isn't found for this PM, create one
+                panel = this.panels.getByName(event.nick);
+                if (!panel) {
+                    panel = new _kiwi.model.Query({name: event.nick, network: this});
+                    this.panels.add(panel);
+                }
+
+            } else {
+                // If a panel isn't found for this target, reroute to the
+                // server panel
+                panel = this.panels.getByName(event.target);
+                if (!panel) {
+                    panel = this.panels.server;
+                }
+            }
+
+            switch (event.type){
+            case 'message':
+                panel.addMsg(event.nick, styleText('privmsg', {text: event.msg}), 'privmsg', {time: event.time});
+                break;
+
+            case 'action':
+                panel.addMsg('', styleText('action', {nick: event.nick, text: event.msg}), 'action', {time: event.time});
+                break;
+
+            case 'notice':
+                panel.addMsg('[' + (event.nick||'') + ']', styleText('notice', {text: event.msg}), 'notice', {time: event.time});
+
+                // Show this notice to the active panel if it didn't have a set target, but only in an active channel or query window
+                active_panel = _kiwi.app.panels().active;
+
+                if (!event.from_server && panel === this.panels.server && active_panel !== this.panels.server) {
+                    if (active_panel.get('network') === this && (active_panel.isChannel() || active_panel.isQuery()))
+                        active_panel.addMsg('[' + (event.nick||'') + ']', styleText('notice', {text: event.msg}), 'notice', {time: event.time});
+                }
+                break;
+            }
+        }, this));
+    }
+
+
+
+    function onNick(event) {
+        var member;
+
+        $.each(this.panels.models, function (index, panel) {
+            if (panel.get('name') == event.nick)
+                panel.set('name', event.newnick);
+
+            if (!panel.isChannel()) return;
+
+            member = panel.get('members').getByNick(event.nick);
+            if (member) {
+                member.set('nick', event.newnick);
+                panel.addMsg('', styleText('nick_changed', {nick: event.nick, text: translateText('client_models_network_nickname_changed', [event.newnick]), channel: name}), 'action nick', {time: event.time});
+            }
+        });
+    }
+
+
+
+    function onCtcpRequest(event) {
+        // An ignored user? don't do anything with it
+        if (this.isNickIgnored(event.nick)) {
+            return;
+        }
+
+        // Reply to a TIME ctcp
+        if (event.msg.toUpperCase() === 'TIME') {
+            this.gateway.ctcpResponse(event.type, event.nick, (new Date()).toString());
+        } else if(event.type.toUpperCase() === 'PING') { // CTCP PING reply
+            this.gateway.ctcpResponse(event.type, event.nick, event.msg.substr(5));
+        }
+    }
+
+
+
+    function onCtcpResponse(event) {
+        // An ignored user? don't do anything with it
+        if (this.isNickIgnored(event.nick)) {
+            return;
+        }
+
+        this.panels.server.addMsg('[' + event.nick + ']',  styleText('ctcp', {text: event.msg}), 'ctcp', {time: event.time});
+    }
+
+
+
+    function onTopic(event) {
+        var c;
+        c = this.panels.getByName(event.channel);
+        if (!c) return;
+
+        // Set the channels topic
+        c.set('topic', event.topic);
+
+        // If this is the active channel, update the topic bar too
+        if (c.get('name') === this.panels.active.get('name')) {
+            _kiwi.app.topicbar.setCurrentTopic(event.topic);
+        }
+    }
+
+
+
+    function onTopicSetBy(event) {
+        var c, when;
+        c = this.panels.getByName(event.channel);
+        if (!c) return;
+
+        when = new Date(event.when * 1000);
+        c.set('topic_set_by', {nick: event.nick, when: when});
+    }
+
+
+
+    function onChannelInfo(event) {
+        var channel = this.panels.getByName(event.channel);
+        if (!channel) return;
+
+        if (event.url) {
+            channel.set('info_url', event.url);
+        } else if (event.modes) {
+            channel.set('info_modes', event.modes);
+        }
+    }
+
+
+
+    function onUserlist(event) {
+        var that = this,
+            channel = this.panels.getByName(event.channel);
+
+        // If we didn't find a channel for this, may aswell leave
+        if (!channel) return;
+
+        channel.temp_userlist = channel.temp_userlist || [];
+        _.each(event.users, function (item) {
+            var user = new _kiwi.model.Member({
+                nick: item.nick,
+                modes: item.modes,
+                user_prefixes: that.get('user_prefixes')
+            });
+            channel.temp_userlist.push(user);
+        });
+    }
+
+
+
+    function onUserlistEnd(event) {
+        var channel;
+        channel = this.panels.getByName(event.channel);
+
+        // If we didn't find a channel for this, may aswell leave
+        if (!channel) return;
+
+        // Update the members list with the new list
+        channel.get('members').reset(channel.temp_userlist || []);
+
+        // Clear the temporary userlist
+        delete channel.temp_userlist;
+    }
+
+
+
+    function onBanlist(event) {
+        var channel = this.panels.getByName(event.channel);
+        if (!channel)
+            return;
+
+        channel.set('banlist', event.bans || []);
+    }
+
+
+
+    function onMode(event) {
+        var channel, i, prefixes, members, member, find_prefix,
+            request_updated_banlist = false;
+
+        // Build a nicely formatted string to be displayed to a regular human
+        function friendlyModeString (event_modes, alt_target) {
+            var modes = {}, return_string;
+
+            // If no default given, use the main event info
+            if (!event_modes) {
+                event_modes = event.modes;
+                alt_target = event.target;
+            }
+
+            // Reformat the mode object to make it easier to work with
+            _.each(event_modes, function (mode){
+                var param = mode.param || alt_target || '';
+
+                // Make sure we have some modes for this param
+                if (!modes[param]) {
+                    modes[param] = {'+':'', '-':''};
+                }
+
+                modes[param][mode.mode[0]] += mode.mode.substr(1);
+            });
+
+            // Put the string together from each mode
+            return_string = [];
+            _.each(modes, function (modeset, param) {
+                var str = '';
+                if (modeset['+']) str += '+' + modeset['+'];
+                if (modeset['-']) str += '-' + modeset['-'];
+                return_string.push(str + ' ' + param);
+            });
+            return_string = return_string.join(', ');
+
+            return return_string;
+        }
+
+
+        channel = this.panels.getByName(event.target);
+        if (channel) {
+            prefixes = this.get('user_prefixes');
+            find_prefix = function (p) {
+                return event.modes[i].mode[1] === p.mode;
+            };
+            for (i = 0; i < event.modes.length; i++) {
+                if (_.any(prefixes, find_prefix)) {
+                    if (!members) {
+                        members = channel.get('members');
+                    }
+                    member = members.getByNick(event.modes[i].param);
+                    if (!member) {
+                        console.log('MODE command recieved for unknown member %s on channel %s', event.modes[i].param, event.target);
+                        return;
+                    } else {
+                        if (event.modes[i].mode[0] === '+') {
+                            member.addMode(event.modes[i].mode[1]);
+                        } else if (event.modes[i].mode[0] === '-') {
+                            member.removeMode(event.modes[i].mode[1]);
+                        }
+                        members.sort();
+                    }
+                } else {
+                    // Channel mode being set
+                    // TODO: Store this somewhere?
+                    //channel.addMsg('', 'CHANNEL === ' + event.nick + ' set mode ' + event.modes[i].mode + ' on ' + event.target, 'action mode');
+                }
+
+                // TODO: Be smart, remove this specific ban from the banlist rather than request a whole banlist
+                if (event.modes[i].mode[1] == 'b')
+                    request_updated_banlist = true;
+            }
+
+            channel.addMsg('', styleText('mode', {nick: event.nick, text: translateText('client_models_network_mode', [friendlyModeString()]), channel: event.target}), 'action mode', {time: event.time});
+
+            // TODO: Be smart, remove the specific ban from the banlist rather than request a whole banlist
+            if (request_updated_banlist)
+                this.gateway.raw('MODE ' + channel.get('name') + ' +b');
+
+        } else {
+            // This is probably a mode being set on us.
+            if (event.target.toLowerCase() === this.get("nick").toLowerCase()) {
+                this.panels.server.addMsg('', styleText('selfmode', {nick: event.nick, text: translateText('client_models_network_mode', [friendlyModeString()]), channel: event.target}), 'action mode');
+            } else {
+               console.log('MODE command recieved for unknown target %s: ', event.target, event);
+            }
+        }
+    }
+
+
+
+    function onWhois(event) {
+        var logon_date, idle_time = '', panel;
+
+        if (event.end)
+            return;
+
+        if (typeof event.idle !== 'undefined') {
+            idle_time = secondsToTime(parseInt(event.idle, 10));
+            idle_time = idle_time.h.toString().lpad(2, "0") + ':' + idle_time.m.toString().lpad(2, "0") + ':' + idle_time.s.toString().lpad(2, "0");
+        }
+
+        panel = _kiwi.app.panels().active;
+        if (event.ident) {
+            panel.addMsg(event.nick, styleText('whois_ident', {nick: event.nick, ident: event.ident, host: event.hostname, text: event.msg}), 'whois');
+
+        } else if (event.chans) {
+            panel.addMsg(event.nick, styleText('whois_channels', {nick: event.nick, text: translateText('client_models_network_channels', [event.chans])}), 'whois');
+        } else if (event.irc_server) {
+            panel.addMsg(event.nick, styleText('whois_server', {nick: event.nick, text: translateText('client_models_network_server', [event.irc_server, event.server_info])}), 'whois');
+        } else if (event.msg) {
+            panel.addMsg(event.nick, styleText('whois', {text: event.msg}), 'whois');
+        } else if (event.logon) {
+            logon_date = new Date();
+            logon_date.setTime(event.logon * 1000);
+            logon_date = _kiwi.utils.formatDate(logon_date);
+
+            panel.addMsg(event.nick, styleText('whois_idle_and_signon', {nick: event.nick, text: translateText('client_models_network_idle_and_signon', [idle_time, logon_date])}), 'whois');
+        } else if (event.away_reason) {
+            panel.addMsg(event.nick, styleText('whois_away', {nick: event.nick, text: translateText('client_models_network_away', [event.away_reason])}), 'whois');
+        } else {
+            panel.addMsg(event.nick, styleText('whois_idle', {nick: event.nick, text: translateText('client_models_network_idle', [idle_time])}), 'whois');
+        }
+    }
+
+    function onWhowas(event) {
+        var panel;
+
+        if (event.end)
+            return;
+
+        panel = _kiwi.app.panels().active;
+        if (event.hostname) {
+            panel.addMsg(event.nick, styleText('who', {nick: event.nick, ident: event.ident, host: event.hostname, realname: event.real_name, text: event.msg}), 'whois');
+        } else {
+            panel.addMsg(event.nick, styleText('whois_notfound', {nick: event.nick, text: translateText('client_models_network_nickname_notfound', [])}), 'whois');
+        }
+    }
+
+
+    function onAway(event) {
+        $.each(this.panels.models, function (index, panel) {
+            if (!panel.isChannel()) return;
+
+            member = panel.get('members').getByNick(event.nick);
+            if (member) {
+                member.set('away', !(!event.reason));
+            }
+        });
+    }
+
+
+
+    function onListStart(event) {
+        var chanlist = _kiwi.model.Applet.loadOnce('kiwi_chanlist');
+        chanlist.view.show();
+    }
+
+
+
+    function onIrcError(event) {
+        var panel, tmp;
+
+        if (event.channel !== undefined && !(panel = this.panels.getByName(event.channel))) {
+            panel = this.panels.server;
+        }
+
+        switch (event.error) {
+        case 'banned_from_channel':
+            panel.addMsg(' ', styleText('channel_banned', {nick: event.nick, text: translateText('client_models_network_banned', [event.channel, event.reason]), channel: event.channel}), 'status');
+            _kiwi.app.message.text(_kiwi.global.i18n.translate('client_models_network_banned').fetch(event.channel, event.reason));
+            break;
+        case 'bad_channel_key':
+            panel.addMsg(' ', styleText('channel_badkey', {nick: event.nick, text: translateText('client_models_network_channel_badkey', [event.channel]), channel: event.channel}), 'status');
+            _kiwi.app.message.text(_kiwi.global.i18n.translate('client_models_network_channel_badkey').fetch(event.channel));
+            break;
+        case 'invite_only_channel':
+            panel.addMsg(' ', styleText('channel_inviteonly', {nick: event.nick, text: translateText('client_models_network_channel_inviteonly', [event.nick, event.channel]), channel: event.channel}), 'status');
+            _kiwi.app.message.text(event.channel + ' ' + _kiwi.global.i18n.translate('client_models_network_channel_inviteonly').fetch());
+            break;
+        case 'user_on_channel':
+            panel.addMsg(' ', styleText('channel_alreadyin', {nick: event.nick, text: translateText('client_models_network_channel_alreadyin'), channel: event.channel}));
+            break;
+        case 'channel_is_full':
+            panel.addMsg(' ', styleText('channel_limitreached', {nick: event.nick, text: translateText('client_models_network_channel_limitreached', [event.channel]), channel: event.channel}), 'status');
+            _kiwi.app.message.text(event.channel + ' ' + _kiwi.global.i18n.translate('client_models_network_channel_limitreached').fetch(event.channel));
+            break;
+        case 'chanop_privs_needed':
+            panel.addMsg(' ', styleText('chanop_privs_needed', {text: event.reason, channel: event.channel}), 'status');
+            _kiwi.app.message.text(event.reason + ' (' + event.channel + ')');
+            break;
+        case 'cannot_send_to_channel':
+            panel.addMsg(' ', '== ' + _kiwi.global.i18n.translate('Cannot send message to channel, you are not voiced').fetch(event.channel, event.reason), 'status');
+            break;
+        case 'no_such_nick':
+            tmp = this.panels.getByName(event.nick);
+            if (tmp) {
+                tmp.addMsg(' ', styleText('no_such_nick', {nick: event.nick, text: event.reason, channel: event.channel}), 'status');
+            } else {
+                this.panels.server.addMsg(' ', styleText('no_such_nick', {nick: event.nick, text: event.reason, channel: event.channel}), 'status');
+            }
+            break;
+        case 'nickname_in_use':
+            this.panels.server.addMsg(' ', styleText('nickname_alreadyinuse', {nick: event.nick, text: translateText('client_models_network_nickname_alreadyinuse', [event.nick]), channel: event.channel}), 'status');
+            if (this.panels.server !== this.panels.active) {
+                _kiwi.app.message.text(_kiwi.global.i18n.translate('client_models_network_nickname_alreadyinuse').fetch(event.nick));
+            }
+
+            // Only show the nickchange component if the controlbox is open
+            if (_kiwi.app.controlbox.$el.css('display') !== 'none') {
+                (new _kiwi.view.NickChangeBox()).render();
+            }
+
+            break;
+
+        case 'password_mismatch':
+            this.panels.server.addMsg(' ', styleText('channel_badpassword', {nick: event.nick, text: translateText('client_models_network_badpassword', []), channel: event.channel}), 'status');
+            break;
+
+        case 'error':
+            if (event.reason) {
+                this.panels.server.addMsg(' ', styleText('general_error', {text: event.reason}), 'status');
+            }
+            break;
+
+        default:
+            // We don't know what data contains, so don't do anything with it.
+            //_kiwi.front.tabviews.server.addMsg(null, ' ', '== ' + data, 'status');
+        }
+    }
+
+
+    function onUnknownCommand(event) {
+        var display_params = _.clone(event.params);
+
+        // A lot of commands have our nick as the first parameter. This is redundant for us
+        if (display_params[0] && display_params[0] == this.get('nick')) {
+            display_params.shift();
+        }
+
+        this.panels.server.addMsg('', styleText('unknown_command', {text: '[' + event.command + '] ' + display_params.join(', ', '')}));
+    }
+
+
+    function onWallops(event) {
+        var active_panel = _kiwi.app.panels().active;
+
+        // Send to server panel
+        this.panels.server.addMsg('[' + (event.nick||'') + ']', styleText('wallops', {text: event.msg}), 'wallops', {time: event.time});
+
+        // Send to active panel if its a channel/query *and* it's related to this network
+        if (active_panel !== this.panels.server && (active_panel.isChannel() || active_panel.isQuery()) && active_panel.get('network') === this)
+            active_panel.addMsg('[' + (event.nick||'') + ']', styleText('wallops', {text: event.msg}), 'wallops', {time: event.time});
+    }
+
+}
+
+)();
+
+
+
+_kiwi.model.Member = Backbone.Model.extend({\r
+    initialize: function (attributes) {\r
+        var nick, modes, prefix;\r
+\r
+        // The nick may have a mode prefix, we don't want this\r
+        nick = this.stripPrefix(this.get("nick"));\r
+\r
+        // Make sure we have a mode array, and that it's sorted\r
+        modes = this.get("modes");\r
+        modes = modes || [];\r
+        this.sortModes(modes);\r
+\r
+        this.set({"nick": nick, "modes": modes, "prefix": this.getPrefix(modes)}, {silent: true});\r
+\r
+        this.updateOpStatus();\r
+\r
+        this.view = new _kiwi.view.Member({"model": this});\r
+    },\r
+\r
+\r
+    /**\r
+     * Sort modes in order of importance\r
+     */\r
+    sortModes: function (modes) {\r
+        var that = this;\r
+\r
+        return modes.sort(function (a, b) {\r
+            var a_idx, b_idx, i;\r
+            var user_prefixes = that.get('user_prefixes');\r
+\r
+            for (i = 0; i < user_prefixes.length; i++) {\r
+                if (user_prefixes[i].mode === a) {\r
+                    a_idx = i;\r
+                }\r
+            }\r
+\r
+            for (i = 0; i < user_prefixes.length; i++) {\r
+                if (user_prefixes[i].mode === b) {\r
+                    b_idx = i;\r
+                }\r
+            }\r
+\r
+            if (a_idx < b_idx) {\r
+                return -1;\r
+            } else if (a_idx > b_idx) {\r
+                return 1;\r
+            } else {\r
+                return 0;\r
+            }\r
+        });\r
+    },\r
+\r
+\r
+    addMode: function (mode) {\r
+        var modes_to_add = mode.split(''),\r
+            modes, prefix;\r
+\r
+        modes = this.get("modes");\r
+        $.each(modes_to_add, function (index, item) {\r
+            modes.push(item);\r
+        });\r
+\r
+        modes = this.sortModes(modes);\r
+        this.set({"prefix": this.getPrefix(modes), "modes": modes});\r
+\r
+        this.updateOpStatus();\r
+\r
+        this.view.render();\r
+    },\r
+\r
+\r
+    removeMode: function (mode) {\r
+        var modes_to_remove = mode.split(''),\r
+            modes, prefix;\r
+\r
+        modes = this.get("modes");\r
+        modes = _.reject(modes, function (m) {\r
+            return (_.indexOf(modes_to_remove, m) !== -1);\r
+        });\r
+\r
+        this.set({"prefix": this.getPrefix(modes), "modes": modes});\r
+\r
+        this.updateOpStatus();\r
+\r
+        this.view.render();\r
+    },\r
+\r
+\r
+    /**\r
+     * Figure out a valid prefix given modes.\r
+     * If a user is an op but also has voice, the prefix\r
+     * should be the op as it is more important.\r
+     */\r
+    getPrefix: function (modes) {\r
+        var prefix = '';\r
+        var user_prefixes = this.get('user_prefixes');\r
+\r
+        if (typeof modes[0] !== 'undefined') {\r
+            prefix = _.detect(user_prefixes, function (prefix) {\r
+                return prefix.mode === modes[0];\r
+            });\r
+\r
+            prefix = (prefix) ? prefix.symbol : '';\r
+        }\r
+\r
+        return prefix;\r
+    },\r
+\r
+\r
+    /**\r
+     * Remove any recognised prefix from a nick\r
+     */\r
+    stripPrefix: function (nick) {\r
+        var tmp = nick, i, j, k, nick_char;\r
+        var user_prefixes = this.get('user_prefixes');\r
+\r
+        i = 0;\r
+\r
+        nick_character_loop:\r
+        for (j = 0; j < nick.length; j++) {\r
+            nick_char = nick.charAt(j);\r
+\r
+            for (k = 0; k < user_prefixes.length; k++) {\r
+                if (nick_char === user_prefixes[k].symbol) {\r
+                    i++;\r
+                    continue nick_character_loop;\r
+                }\r
+            }\r
+\r
+            break;\r
+        }\r
+\r
+        return tmp.substr(i);\r
+    },\r
+\r
+\r
+\r
+    /**\r
+     * Format this nick into readable format (eg. nick [ident@hostname])\r
+     */\r
+    displayNick: function (full) {\r
+        var display = this.get('nick');\r
+\r
+        if (full) {\r
+            if (this.get("ident")) {\r
+                display += ' [' + this.get("ident") + '@' + this.get("hostname") + ']';\r
+            }\r
+        }\r
+\r
+        return display;\r
+    },\r
+\r
+\r
+    // Helper to quickly get user mask details\r
+    getMaskParts: function () {\r
+        return {\r
+            nick: this.get('nick') || '',\r
+            ident: this.get('ident') || '',\r
+            hostname: this.get('hostname') || ''\r
+        };\r
+    },\r
+\r
+\r
+    /**\r
+     * With the modes set on the user, make note if we have some sort of op status\r
+     */\r
+    updateOpStatus: function () {\r
+        var user_prefixes = this.get('user_prefixes'),\r
+            modes = this.get('modes'),\r
+            o, max_mode;\r
+\r
+        if (modes.length > 0) {\r
+            o = _.indexOf(user_prefixes, _.find(user_prefixes, function (prefix) {\r
+                return prefix.mode === 'o';\r
+            }));\r
+\r
+            max_mode = _.indexOf(user_prefixes, _.find(user_prefixes, function (prefix) {\r
+                return prefix.mode === modes[0];\r
+            }));\r
+\r
+            if ((max_mode === -1) || (max_mode > o)) {\r
+                this.set({"is_op": false}, {silent: true});\r
+            } else {\r
+                this.set({"is_op": true}, {silent: true});\r
+            }\r
+\r
+        } else {\r
+            this.set({"is_op": false}, {silent: true});\r
+        }\r
+    }\r
+});
+
+
+_kiwi.model.MemberList = Backbone.Collection.extend({\r
+    model: _kiwi.model.Member,\r
+    comparator: function (a, b) {\r
+        var i, a_modes, b_modes, a_idx, b_idx, a_nick, b_nick;\r
+        var user_prefixes = this.channel.get('network').get('user_prefixes');\r
+\r
+        a_modes = a.get("modes");\r
+        b_modes = b.get("modes");\r
+\r
+        // Try to sort by modes first\r
+        if (a_modes.length > 0) {\r
+            // a has modes, but b doesn't so a should appear first\r
+            if (b_modes.length === 0) {\r
+                return -1;\r
+            }\r
+            a_idx = b_idx = -1;\r
+            // Compare the first (highest) mode\r
+            for (i = 0; i < user_prefixes.length; i++) {\r
+                if (user_prefixes[i].mode === a_modes[0]) {\r
+                    a_idx = i;\r
+                }\r
+            }\r
+            for (i = 0; i < user_prefixes.length; i++) {\r
+                if (user_prefixes[i].mode === b_modes[0]) {\r
+                    b_idx = i;\r
+                }\r
+            }\r
+            if (a_idx < b_idx) {\r
+                return -1;\r
+            } else if (a_idx > b_idx) {\r
+                return 1;\r
+            }\r
+            // If we get to here both a and b have the same highest mode so have to resort to lexicographical sorting\r
+\r
+        } else if (b_modes.length > 0) {\r
+            // b has modes but a doesn't so b should appear first\r
+            return 1;\r
+        }\r
+        a_nick = a.get("nick").toLocaleUpperCase();\r
+        b_nick = b.get("nick").toLocaleUpperCase();\r
+        // Lexicographical sorting\r
+        if (a_nick < b_nick) {\r
+            return -1;\r
+        } else if (a_nick > b_nick) {\r
+            return 1;\r
+        } else {\r
+            return 0;\r
+        }\r
+    },\r
+\r
+\r
+    initialize: function (options) {\r
+        this.view = new _kiwi.view.MemberList({"model": this});\r
+        this.initNickCache();\r
+    },\r
+\r
+\r
+    /*\r
+     * Keep a reference to each member by the nick. Speeds up .getByNick()\r
+     * so it doesn't need to loop over every model for each nick lookup\r
+     */\r
+    initNickCache: function() {\r
+        var that = this;\r
+\r
+        this.nick_cache = Object.create(null);\r
+\r
+        this.on('reset', function() {\r
+            this.nick_cache = Object.create(null);\r
+\r
+            this.models.forEach(function(member) {\r
+                that.nick_cache[member.get('nick').toLowerCase()] = member;\r
+            });\r
+        });\r
+\r
+        this.on('add', function(member) {\r
+            that.nick_cache[member.get('nick').toLowerCase()] = member;\r
+        });\r
+\r
+        this.on('remove', function(member) {\r
+            delete that.nick_cache[member.get('nick').toLowerCase()];\r
+        });\r
+\r
+        this.on('change:nick', function(member) {\r
+            that.nick_cache[member.get('nick').toLowerCase()] = member;\r
+            delete that.nick_cache[member.previous('nick').toLowerCase()];\r
+        });\r
+    },\r
+\r
+\r
+    getByNick: function (nick) {\r
+        if (typeof nick !== 'string') return;\r
+        return this.nick_cache[nick.toLowerCase()];\r
+    }\r
+});
+
+
+_kiwi.model.NewConnection = Backbone.Collection.extend({
+    initialize: function() {
+        this.view = new _kiwi.view.ServerSelect({model: this});
+
+        this.view.bind('server_connect', this.onMakeConnection, this);
+
+    },
+
+
+    populateDefaultServerSettings: function() {
+        var defaults = _kiwi.global.defaultServerSettings();
+        this.view.populateFields(defaults);
+    },
+
+
+    onMakeConnection: function(new_connection_event) {
+        var that = this;
+
+        this.connect_details = new_connection_event;
+
+        this.view.networkConnecting();
+
+        _kiwi.gateway.newConnection({
+            nick: new_connection_event.nick,
+            host: new_connection_event.server,
+            port: new_connection_event.port,
+            ssl: new_connection_event.ssl,
+            password: new_connection_event.password,
+            options: new_connection_event.options
+        }, function(err, network) {
+            that.onNewNetwork(err, network);
+        });
+    },
+
+
+    onNewNetwork: function(err, network) {
+        // Show any errors if given
+        if (err) {
+            this.view.showError(err);
+        }
+
+        if (network && this.connect_details) {
+            network.auto_join = {
+                channel: this.connect_details.channel,
+                key: this.connect_details.channel_key
+            };
+
+            this.trigger('new_network', network);
+        }
+    }
+});
+
+
+_kiwi.model.Panel = Backbone.Model.extend({\r
+    initialize: function (attributes) {\r
+        var name = this.get("name") || "";\r
+        this.view = new _kiwi.view.Panel({"model": this, "name": name});\r
+        this.set({\r
+            "scrollback": [],\r
+            "name": name\r
+        }, {"silent": true});\r
+\r
+        _kiwi.global.events.emit('panel:created', {panel: this});\r
+    },\r
+\r
+    close: function () {\r
+        _kiwi.app.panels.trigger('close', this);\r
+        _kiwi.global.events.emit('panel:close', {panel: this});\r
+\r
+        if (this.view) {\r
+            this.view.unbind();\r
+            this.view.remove();\r
+            this.view = undefined;\r
+            delete this.view;\r
+        }\r
+\r
+        var members = this.get('members');\r
+        if (members) {\r
+            members.reset([]);\r
+            this.unset('members');\r
+        }\r
+\r
+        this.get('panel_list').remove(this);\r
+\r
+        this.unbind();\r
+        this.destroy();\r
+    },\r
+\r
+    isChannel: function () {\r
+        return false;\r
+    },\r
+\r
+    isQuery: function () {\r
+        return false;\r
+    },\r
+\r
+    isApplet: function () {\r
+        return false;\r
+    },\r
+\r
+    isServer: function () {\r
+        return false;\r
+    },\r
+\r
+    isActive: function () {\r
+        return (_kiwi.app.panels().active === this);\r
+    }\r
+});
+
+
+_kiwi.model.PanelList = Backbone.Collection.extend({\r
+    model: _kiwi.model.Panel,\r
+\r
+    comparator: function (chan) {\r
+        return chan.get('name');\r
+    },\r
+    initialize: function (elements, network) {\r
+        var that = this;\r
+\r
+        // If this PanelList is associated with a network/connection\r
+        if (network) {\r
+            this.network = network;\r
+        }\r
+\r
+        this.view = new _kiwi.view.Tabs({model: this});\r
+\r
+        // Holds the active panel\r
+        this.active = null;\r
+\r
+        // Keep a tab on the active panel\r
+        this.bind('active', function (active_panel) {\r
+            this.active = active_panel;\r
+        }, this);\r
+\r
+        this.bind('add', function(panel) {\r
+            panel.set('panel_list', this);\r
+        });\r
+    },\r
+\r
+\r
+\r
+    getByCid: function (cid) {\r
+        if (typeof name !== 'string') return;\r
+\r
+        return this.find(function (c) {\r
+            return cid === c.cid;\r
+        });\r
+    },\r
+\r
+\r
+\r
+    getByName: function (name) {\r
+        if (typeof name !== 'string') return;\r
+\r
+        return this.find(function (c) {\r
+            return name.toLowerCase() === c.get('name').toLowerCase();\r
+        });\r
+    }\r
+});\r
+
+
+
+_kiwi.model.NetworkPanelList = Backbone.Collection.extend({
+    model: _kiwi.model.Network,
+
+    initialize: function() {
+        this.view = new _kiwi.view.NetworkTabs({model: this});
+        
+        this.on('add', this.onNetworkAdd, this);
+        this.on('remove', this.onNetworkRemove, this);
+
+        // Current active connection / panel
+        this.active_connection = undefined;
+        this.active_panel = undefined;
+
+        // TODO: Remove this - legacy
+        this.active = undefined;
+    },
+
+    getByConnectionId: function(id) {
+        return this.find(function(connection){
+            return connection.get('connection_id') == id;
+        });
+    },
+
+    panels: function() {
+        var panels = [];
+
+        this.each(function(network) {
+            panels = panels.concat(network.panels.models);
+        });
+
+        return panels;
+    },
+
+
+    onNetworkAdd: function(network) {
+        network.panels.on('active', this.onPanelActive, this);
+
+        // if it's our first connection, set it active
+        if (this.models.length === 1) {
+            this.active_connection = network;
+            this.active_panel = network.panels.server;
+
+            // TODO: Remove this - legacy
+            this.active = this.active_panel;
+        }
+    },
+
+    onNetworkRemove: function(network) {
+        network.panels.off('active', this.onPanelActive, this);
+    },
+
+    onPanelActive: function(panel) {
+        var connection = this.getByConnectionId(panel.tab.data('connection_id'));
+        this.trigger('active', panel, connection);
+
+        this.active_connection = connection;
+        this.active_panel = panel;
+
+        // TODO: Remove this - legacy
+        this.active = panel;
+    }
+});
+
+
+// TODO: Channel modes\r
+// TODO: Listen to gateway events for anythign related to this channel\r
+_kiwi.model.Channel = _kiwi.model.Panel.extend({\r
+    initialize: function (attributes) {\r
+        var name = this.get("name") || "",\r
+            members;\r
+\r
+        this.set({\r
+            "members": new _kiwi.model.MemberList(),\r
+            "name": name,\r
+            "scrollback": [],\r
+            "topic": ""\r
+        }, {"silent": true});\r
+\r
+        this.view = new _kiwi.view.Channel({"model": this, "name": name});\r
+\r
+        members = this.get("members");\r
+        members.channel = this;\r
+        members.bind("add", function (member, members, options) {\r
+            var show_message = _kiwi.global.settings.get('show_joins_parts');\r
+            if (show_message === false) {\r
+                return;\r
+            }\r
+\r
+            this.addMsg(' ', styleText('channel_join', {member: member.getMaskParts(), text: translateText('client_models_channel_join'), channel: name}), 'action join', {time: options.kiwi.time});\r
+        }, this);\r
+\r
+        members.bind("remove", function (member, members, options) {\r
+            var show_message = _kiwi.global.settings.get('show_joins_parts');\r
+            var msg = (options.kiwi.message) ? '(' + options.kiwi.message + ')' : '';\r
+\r
+            if (options.kiwi.type === 'quit' && show_message) {\r
+                this.addMsg(' ', styleText('channel_quit', {member: member.getMaskParts(), text: translateText('client_models_channel_quit', [msg]), channel: name}), 'action quit', {time: options.kiwi.time});\r
+\r
+            } else if (options.kiwi.type === 'kick') {\r
+\r
+                if (!options.kiwi.current_user_kicked) {\r
+                    //If user kicked someone, show the message regardless of settings.\r
+                    if (show_message || options.kiwi.current_user_initiated) {\r
+                        this.addMsg(' ', styleText('channel_kicked', {member: member.getMaskParts(), text: translateText('client_models_channel_kicked', [options.kiwi.by, msg]), channel: name}), 'action kick', {time: options.kiwi.time});\r
+                    }\r
+                } else {\r
+                    this.addMsg(' ', styleText('channel_selfkick', {text: translateText('client_models_channel_selfkick', [options.kiwi.by, msg]), channel: name}), 'action kick', {time: options.kiwi.time});\r
+                }\r
+            } else if (show_message) {\r
+                this.addMsg(' ', styleText('channel_part', {member: member.getMaskParts(), text: translateText('client_models_channel_part', [msg]), channel: name}), 'action part', {time: options.kiwi.time});\r
+\r
+            }\r
+        }, this);\r
+\r
+        _kiwi.global.events.emit('panel:created', {panel: this});\r
+    },\r
+\r
+\r
+    addMsg: function (nick, msg, type, opts) {\r
+        var message_obj, bs, d, members, member,\r
+            scrollback = (parseInt(_kiwi.global.settings.get('scrollback'), 10) || 250);\r
+\r
+        opts = opts || {};\r
+\r
+        // Time defaults to now\r
+        if (typeof opts.time === 'number') {\r
+            opts.time = new Date(opts.time);\r
+        } else {\r
+            opts.time = new Date();\r
+        }\r
+\r
+        // CSS style defaults to empty string\r
+        if (!opts || typeof opts.style === 'undefined') {\r
+            opts.style = '';\r
+        }\r
+\r
+        // Create a message object\r
+        message_obj = {"msg": msg, "date": opts.date, "time": opts.time, "nick": nick, "chan": this.get("name"), "type": type, "style": opts.style};\r
+\r
+        // If this user has one, get its prefix\r
+        members = this.get('members');\r
+        if (members) {\r
+            member = members.getByNick(message_obj.nick);\r
+            if (member) {\r
+                message_obj.nick_prefix = member.get('prefix');\r
+            }\r
+        }\r
+\r
+        // The CSS class (action, topic, notice, etc)\r
+        if (typeof message_obj.type !== "string") {\r
+            message_obj.type = '';\r
+        }\r
+\r
+        // Make sure we don't have NaN or something\r
+        if (typeof message_obj.msg !== "string") {\r
+            message_obj.msg = '';\r
+        }\r
+\r
+        // Update the scrollback\r
+        bs = this.get("scrollback");\r
+        if (bs) {\r
+            bs.push(message_obj);\r
+\r
+            // Keep the scrolback limited\r
+            if (bs.length > scrollback) {\r
+                bs = _.last(bs, scrollback);\r
+            }\r
+            this.set({"scrollback": bs}, {silent: true});\r
+        }\r
+\r
+        this.trigger("msg", message_obj);\r
+    },\r
+\r
+\r
+    clearMessages: function () {\r
+        this.set({'scrollback': []}, {silent: true});\r
+        this.addMsg('', 'Window cleared');\r
+\r
+        this.view.render();\r
+    },\r
+\r
+\r
+    setMode: function(mode_string) {\r
+        this.get('network').gateway.mode(this.get('name'), mode_string);\r
+    },\r
+\r
+    isChannel: function() {\r
+        return true;\r
+    }\r
+});\r
+
+
+
+_kiwi.model.Query = _kiwi.model.Channel.extend({\r
+    initialize: function (attributes) {\r
+        var name = this.get("name") || "",\r
+            members;\r
+\r
+        this.view = new _kiwi.view.Channel({"model": this, "name": name});\r
+        this.set({\r
+            "name": name,\r
+            "scrollback": []\r
+        }, {"silent": true});\r
+\r
+        _kiwi.global.events.emit('panel:created', {panel: this});\r
+    },\r
+\r
+    isChannel: function () {\r
+        return false;\r
+    },\r
+\r
+    isQuery: function () {\r
+        return true;\r
+    }\r
+});
+
+
+_kiwi.model.Server = _kiwi.model.Channel.extend({\r
+    initialize: function (attributes) {\r
+        var name = "Server";\r
+        this.view = new _kiwi.view.Channel({"model": this, "name": name});\r
+        this.set({\r
+            "scrollback": [],\r
+            "name": name\r
+        }, {"silent": true});\r
+\r
+        _kiwi.global.events.emit('panel:created', {panel: this});\r
+    },\r
+\r
+    isServer: function () {\r
+        return true;\r
+    },\r
+\r
+    isChannel: function () {\r
+        return false;\r
+    }\r
+});
+
+
+_kiwi.model.Applet = _kiwi.model.Panel.extend({\r
+    initialize: function (attributes) {\r
+        // Temporary name\r
+        var name = "applet_"+(new Date().getTime().toString()) + Math.ceil(Math.random()*100).toString();\r
+        this.view = new _kiwi.view.Applet({model: this, name: name});\r
+\r
+        this.set({\r
+            "name": name\r
+        }, {"silent": true});\r
+\r
+        // Holds the loaded applet\r
+        this.loaded_applet = null;\r
+    },\r
+\r
+\r
+    // Load an applet within this panel\r
+    load: function (applet_object, applet_name) {\r
+        if (typeof applet_object === 'object') {\r
+            // Make sure this is a valid Applet\r
+            if (applet_object.get || applet_object.extend) {\r
+\r
+                // Try find a title for the applet\r
+                this.set('title', applet_object.get('title') || _kiwi.global.i18n.translate('client_models_applet_unknown').fetch());\r
+\r
+                // Update the tabs title if the applet changes it\r
+                applet_object.bind('change:title', function (obj, new_value) {\r
+                    this.set('title', new_value);\r
+                }, this);\r
+\r
+                // If this applet has a UI, add it now\r
+                this.view.$el.html('');\r
+                if (applet_object.view) {\r
+                    this.view.$el.append(applet_object.view.$el);\r
+                }\r
+\r
+                // Keep a reference to this applet\r
+                this.loaded_applet = applet_object;\r
+\r
+                this.loaded_applet.trigger('applet_loaded');\r
+            }\r
+\r
+        } else if (typeof applet_object === 'string') {\r
+            // Treat this as a URL to an applet script and load it\r
+            this.loadFromUrl(applet_object, applet_name);\r
+        }\r
+\r
+        return this;\r
+    },\r
+\r
+\r
+    loadFromUrl: function(applet_url, applet_name) {\r
+        var that = this;\r
+\r
+        this.view.$el.html(_kiwi.global.i18n.translate('client_models_applet_loading').fetch());\r
+        $script(applet_url, function () {\r
+            // Check if the applet loaded OK\r
+            if (!_kiwi.applets[applet_name]) {\r
+                that.view.$el.html(_kiwi.global.i18n.translate('client_models_applet_notfound').fetch());\r
+                return;\r
+            }\r
+\r
+            // Load a new instance of this applet\r
+            that.load(new _kiwi.applets[applet_name]());\r
+        });\r
+    },\r
+\r
+\r
+    close: function () {\r
+        this.view.$el.remove();\r
+        this.destroy();\r
+\r
+        this.view = undefined;\r
+\r
+        // Call the applets dispose method if it has one\r
+        if (this.loaded_applet && this.loaded_applet.dispose) {\r
+            this.loaded_applet.dispose();\r
+        }\r
+\r
+        // Call the inherited close()\r
+        this.constructor.__super__.close.apply(this, arguments);\r
+    },\r
+\r
+    isApplet: function () {\r
+        return true;\r
+    }\r
+},\r
+\r
+\r
+{\r
+    // Load an applet type once only. If it already exists, return that\r
+    loadOnce: function (applet_name) {\r
+\r
+        // See if we have an instance loaded already\r
+        var applet = _.find(_kiwi.app.panels('applets'), function(panel) {\r
+            // Ignore if it's not an applet\r
+            if (!panel.isApplet()) return;\r
+\r
+            // Ignore if it doesn't have an applet loaded\r
+            if (!panel.loaded_applet) return;\r
+\r
+            if (panel.loaded_applet.get('_applet_name') === applet_name) {\r
+                return true;\r
+            }\r
+        });\r
+\r
+        if (applet) return applet;\r
+\r
+\r
+        // If we didn't find an instance, load a new one up\r
+        return this.load(applet_name);\r
+    },\r
+\r
+\r
+    load: function (applet_name, options) {\r
+        var applet, applet_obj;\r
+\r
+        options = options || {};\r
+\r
+        applet_obj = this.getApplet(applet_name);\r
+\r
+        if (!applet_obj)\r
+            return;\r
+\r
+        // Create the applet and load the content\r
+        applet = new _kiwi.model.Applet();\r
+        applet.load(new applet_obj({_applet_name: applet_name}));\r
+\r
+        // Add it into the tab list if needed (default)\r
+        if (!options.no_tab)\r
+            _kiwi.app.applet_panels.add(applet);\r
+\r
+\r
+        return applet;\r
+    },\r
+\r
+\r
+    getApplet: function (applet_name) {\r
+        return _kiwi.applets[applet_name] || null;\r
+    },\r
+\r
+\r
+    register: function (applet_name, applet) {\r
+        _kiwi.applets[applet_name] = applet;\r
+    }\r
+});
+
+
+_kiwi.model.PluginManager = Backbone.Model.extend({\r
+    initialize: function () {\r
+        this.$plugin_holder = $('<div id="kiwi_plugins" style="display:none;"></div>')\r
+            .appendTo(_kiwi.app.view.$el);\r
+\r
+        this.loading_plugins = 0;\r
+        this.loaded_plugins = {};\r
+    },\r
+\r
+    // Load an applet within this panel\r
+    load: function (url) {\r
+        var that = this;\r
+\r
+        if (this.loaded_plugins[url]) {\r
+            this.unload(url);\r
+        }\r
+\r
+        this.loading_plugins++;\r
+\r
+        this.loaded_plugins[url] = $('<div></div>');\r
+        this.loaded_plugins[url].appendTo(this.$plugin_holder)\r
+            .load(url, _.bind(that.pluginLoaded, that));\r
+    },\r
+\r
+\r
+    unload: function (url) {\r
+        if (!this.loaded_plugins[url]) {\r
+            return;\r
+        }\r
+\r
+        this.loaded_plugins[url].remove();\r
+        delete this.loaded_plugins[url];\r
+    },\r
+\r
+\r
+    // Called after each plugin is loaded\r
+    pluginLoaded: function() {\r
+        this.loading_plugins--;\r
+\r
+        if (this.loading_plugins === 0) {\r
+            this.trigger('loaded');\r
+        }\r
+    },\r
+});
+
+
+_kiwi.model.DataStore = Backbone.Model.extend({
+       initialize: function () {
+               this._namespace = '';
+               this.new_data = {};
+       },
+
+       namespace: function (new_namespace) {
+               if (new_namespace) this._namespace = new_namespace;
+               return this._namespace;
+       },
+
+       // Overload the original save() method
+       save: function () {
+               localStorage.setItem(this._namespace, JSON.stringify(this.attributes));
+       },
+
+       // Overload the original load() method
+       load: function () {
+               if (!localStorage) return;
+
+               var data;
+
+               try {
+                       data = JSON.parse(localStorage.getItem(this._namespace)) || {};
+               } catch (error) {
+                       data = {};
+               }
+
+               this.attributes = data;
+       }
+},
+
+{
+       // Generates a new instance of DataStore with a set namespace
+       instance: function (namespace, attributes) {
+               var datastore = new _kiwi.model.DataStore(attributes);
+               datastore.namespace(namespace);
+               return datastore;
+       }
+});
+
+
+_kiwi.model.ChannelInfo = Backbone.Model.extend({
+    initialize: function () {
+        this.view = new _kiwi.view.ChannelInfo({"model": this});
+    }
+});
+
+
+_kiwi.view.Panel = Backbone.View.extend({
+    tagName: "div",
+    className: "panel",
+
+    events: {
+    },
+
+    initialize: function (options) {
+        this.initializePanel(options);
+    },
+
+    initializePanel: function (options) {
+        this.$el.css('display', 'none');
+        options = options || {};
+
+        // Containing element for this panel
+        if (options.container) {
+            this.$container = $(options.container);
+        } else {
+            this.$container = $('#kiwi .panels .container1');
+        }
+
+        this.$el.appendTo(this.$container);
+
+        this.alert_level = 0;
+
+        this.model.set({"view": this}, {"silent": true});
+
+        this.listenTo(this.model, 'change:activity_counter', function(model, new_count) {
+            var $act = this.model.tab.find('.activity');
+
+            if (new_count > 999) {
+                $act.text('999+');
+            } else {
+                $act.text(new_count);
+            }
+
+            if (new_count === 0) {
+                $act.addClass('zero');
+            } else {
+                $act.removeClass('zero');
+            }
+        });
+    },
+
+    render: function () {
+    },
+
+
+    show: function () {
+        var $this = this.$el;
+
+        // Hide all other panels and show this one
+        this.$container.children('.panel').css('display', 'none');
+        $this.css('display', 'block');
+
+        // Show this panels memberlist
+        var members = this.model.get("members");
+        if (members) {
+            _kiwi.app.rightbar.show();
+            members.view.show();
+        } else {
+            _kiwi.app.rightbar.hide();
+        }
+
+        // Remove any alerts and activity counters for this panel
+        this.alert('none');
+        this.model.set('activity_counter', 0);
+
+        _kiwi.app.panels.trigger('active', this.model, _kiwi.app.panels().active);
+        this.model.trigger('active', this.model);
+
+        _kiwi.app.view.doLayout();
+
+        if (!this.model.isApplet())
+            this.scrollToBottom(true);
+    },
+
+
+    alert: function (level) {
+        // No need to highlight if this si the active panel
+        if (this.model == _kiwi.app.panels().active) return;
+
+        var types, type_idx;
+        types = ['none', 'action', 'activity', 'highlight'];
+
+        // Default alert level
+        level = level || 'none';
+
+        // If this alert level does not exist, assume clearing current level
+        type_idx = _.indexOf(types, level);
+        if (!type_idx) {
+            level = 'none';
+            type_idx = 0;
+        }
+
+        // Only 'upgrade' the alert. Never down (unless clearing)
+        if (type_idx !== 0 && type_idx <= this.alert_level) {
+            return;
+        }
+
+        // Clear any existing levels
+        this.model.tab.removeClass(function (i, css) {
+            return (css.match(/\balert_\S+/g) || []).join(' ');
+        });
+
+        // Add the new level if there is one
+        if (level !== 'none') {
+            this.model.tab.addClass('alert_' + level);
+        }
+
+        this.alert_level = type_idx;
+    },
+
+
+    // Scroll to the bottom of the panel
+    scrollToBottom: function (force_down) {
+        // If this isn't the active panel, don't scroll
+        if (this.model !== _kiwi.app.panels().active) return;
+
+        // Don't scroll down if we're scrolled up the panel a little
+        if (force_down || this.$container.scrollTop() + this.$container.height() > this.$el.outerHeight() - 150) {
+            this.$container[0].scrollTop = this.$container[0].scrollHeight;
+        }
+    }
+});
+
+
+_kiwi.view.Channel = _kiwi.view.Panel.extend({
+    events: function(){
+        var parent_events = this.constructor.__super__.events;
+
+        if(_.isFunction(parent_events)){
+            parent_events = parent_events();
+        }
+        return _.extend({}, parent_events, {
+            'click .msg .nick' : 'nickClick',
+            'click .msg .inline-nick' : 'nickClick',
+            "click .chan": "chanClick",
+            'click .media .open': 'mediaClick',
+            'mouseenter .msg .nick': 'msgEnter',
+            'mouseleave .msg .nick': 'msgLeave'
+        });
+    },
+
+    initialize: function (options) {
+        this.initializePanel(options);
+
+        // Container for all the messages
+        this.$messages = $('<div class="messages"></div>');
+        this.$el.append(this.$messages);
+
+        this.model.bind('change:topic', this.topic, this);
+        this.model.bind('change:topic_set_by', this.topicSetBy, this);
+
+        if (this.model.get('members')) {
+            // When we join the memberlist, we have officially joined the channel
+            this.model.get('members').bind('add', function (member) {
+                if (member.get('nick') === this.model.collection.network.get('nick')) {
+                    this.$el.find('.initial_loader').slideUp(function () {
+                        $(this).remove();
+                    });
+                }
+            }, this);
+
+            // Memberlist reset with a new nicklist? Consider we have joined
+            this.model.get('members').bind('reset', function(members) {
+                if (members.getByNick(this.model.collection.network.get('nick'))) {
+                    this.$el.find('.initial_loader').slideUp(function () {
+                        $(this).remove();
+                    });
+                }
+            }, this);
+        }
+
+        // Only show the loader if this is a channel (ie. not a query)
+        if (this.model.isChannel()) {
+            this.$el.append('<div class="initial_loader" style="margin:1em;text-align:center;"> ' + _kiwi.global.i18n.translate('client_views_channel_joining').fetch() + ' <span class="loader"></span></div>');
+        }
+
+        this.model.bind('msg', this.newMsg, this);
+        this.msg_count = 0;
+    },
+
+
+    render: function () {
+        var that = this;
+
+        this.$messages.empty();
+        _.each(this.model.get('scrollback'), function (msg) {
+            that.newMsg(msg);
+        });
+    },
+
+
+    newMsg: function(msg) {
+
+        // Parse the msg object into properties fit for displaying
+        msg = this.generateMessageDisplayObj(msg);
+
+        _kiwi.global.events.emit('message:display', {panel: this.model, message: msg})
+        .then(_.bind(function() {
+            var line_msg;
+
+            // Format the nick to the config defined format
+            var display_obj = _.clone(msg);
+            display_obj.nick = styleText('message_nick', {nick: msg.nick, prefix: msg.nick_prefix || ''});
+
+            line_msg = '<div class="msg <%= type %> <%= css_classes %>"><div class="time"><%- time_string %></div><div class="nick" style="<%= nick_style %>"><%- nick %></div><div class="text" style="<%= style %>"><%= msg %> </div></div>';
+            this.$messages.append($(_.template(line_msg, display_obj)).data('message', msg));
+
+            // Activity/alerts based on the type of new message
+            if (msg.type.match(/^action /)) {
+                this.alert('action');
+
+            } else if (msg.is_highlight) {
+                _kiwi.app.view.alertWindow('* ' + _kiwi.global.i18n.translate('client_views_panel_activity').fetch());
+                _kiwi.app.view.favicon.newHighlight();
+                _kiwi.app.view.playSound('highlight');
+                _kiwi.app.view.showNotification(this.model.get('name'), msg.unparsed_msg);
+                this.alert('highlight');
+
+            } else {
+                // If this is the active panel, send an alert out
+                if (this.model.isActive()) {
+                    _kiwi.app.view.alertWindow('* ' + _kiwi.global.i18n.translate('client_views_panel_activity').fetch());
+                }
+                this.alert('activity');
+            }
+
+            if (this.model.isQuery() && !this.model.isActive()) {
+                _kiwi.app.view.alertWindow('* ' + _kiwi.global.i18n.translate('client_views_panel_activity').fetch());
+
+                // Highlights have already been dealt with above
+                if (!msg.is_highlight) {
+                    _kiwi.app.view.favicon.newHighlight();
+                }
+
+                _kiwi.app.view.showNotification(this.model.get('name'), msg.unparsed_msg);
+                _kiwi.app.view.playSound('highlight');
+            }
+
+            // Update the activity counters
+            (function () {
+                // Only inrement the counters if we're not the active panel
+                if (this.model.isActive()) return;
+
+                var count_all_activity = _kiwi.global.settings.get('count_all_activity'),
+                    exclude_message_types, new_count;
+
+                // Set the default config value
+                if (typeof count_all_activity === 'undefined') {
+                    count_all_activity = false;
+                }
+
+                // Do not increment the counter for these message types
+                exclude_message_types = [
+                    'action join',
+                    'action quit',
+                    'action part',
+                    'action kick',
+                    'action nick',
+                    'action mode'
+                ];
+
+                if (count_all_activity || _.indexOf(exclude_message_types, msg.type) === -1) {
+                    new_count = this.model.get('activity_counter') || 0;
+                    new_count++;
+                    this.model.set('activity_counter', new_count);
+                }
+
+            }).apply(this);
+
+            if(this.model.isActive()) this.scrollToBottom();
+
+            // Make sure our DOM isn't getting too large (Acts as scrollback)
+            this.msg_count++;
+            if (this.msg_count > (parseInt(_kiwi.global.settings.get('scrollback'), 10) || 250)) {
+                $('.msg:first', this.$messages).remove();
+                this.msg_count--;
+            }
+        }, this));
+    },
+
+
+    // Let nicks be clickable + colourise within messages
+    parseMessageNicks: function(word, colourise) {
+        var members, member, style = '';
+
+        members = this.model.get('members');
+        if (!members) {
+            return;
+        }
+
+        member = members.getByNick(word);
+        if (!member) {
+            return;
+        }
+
+        if (colourise !== false) {
+            // Use the nick from the member object so the style matches the letter casing
+            style = this.getNickStyles(member.get('nick')).asCssString();
+        }
+
+        return _.template('<span class="inline-nick" style="<%- style %>;cursor:pointer;" data-nick="<%- nick %>"><%- nick %></span>', {
+            nick: word,
+            style: style
+        });
+
+    },
+
+
+    // Make channels clickable
+    parseMessageChannels: function(word) {
+        var re,
+            parsed = false,
+            network = this.model.get('network');
+
+        if (!network) {
+            return;
+        }
+
+        re = new RegExp('(^|\\s)([' + escapeRegex(network.get('channel_prefix')) + '][^ ,\\007]+)', 'g');
+
+        if (!word.match(re)) {
+            return parsed;
+        }
+
+        parsed = word.replace(re, function (m1, m2) {
+            return m2 + '<a class="chan" data-channel="' + _.escape(m1.trim()) + '">' + _.escape(m1.trim()) + '</a>';
+        });
+
+        return parsed;
+    },
+
+
+    parseMessageUrls: function(word) {
+        var found_a_url = false,
+            parsed_url;
+
+        parsed_url = word.replace(/^(([A-Za-z][A-Za-z0-9\-]*\:\/\/)|(www\.))([\w.\-]+)([a-zA-Z]{2,6})(:[0-9]+)?(\/[\w!:.?$'()[\]*,;~+=&%@!\-\/]*)?(#.*)?$/gi, function (url) {
+            var nice = url,
+                extra_html = '';
+
+            // Don't allow javascript execution
+            if (url.match(/^javascript:/)) {
+                return url;
+            }
+
+            found_a_url = true;
+
+            // Add the http if no protoocol was found
+            if (url.match(/^www\./)) {
+                url = 'http://' + url;
+            }
+
+            // Shorten the displayed URL if it's going to be too long
+            if (nice.length > 100) {
+                nice = nice.substr(0, 100) + '...';
+            }
+
+            // Get any media HTML if supported
+            extra_html = _kiwi.view.MediaMessage.buildHtml(url);
+
+            // Make the link clickable
+            return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url.replace(/"/g, '%22') + '">' + _.escape(nice) + '</a>' + extra_html;
+        });
+
+        return found_a_url ? parsed_url : false;
+    },
+
+
+    // Sgnerate a css style for a nick
+    getNickStyles: function(nick) {
+        var ret, colour, nick_int = 0, rgb, nick_lightness;
+
+        // Get a colour from a nick (Method based on IRSSIs nickcolor.pl)
+        _.map(nick.split(''), function (i) { nick_int += i.charCodeAt(0); });
+
+        nick_lightness = (_.find(_kiwi.app.themes, function (theme) {
+            return theme.name.toLowerCase() === _kiwi.global.settings.get('theme').toLowerCase();
+        }) || {}).nick_lightness;
+
+        if (typeof nick_lightness !== 'number') {
+            nick_lightness = 35;
+        } else {
+            nick_lightness = Math.max(0, Math.min(100, nick_lightness));
+        }
+
+        rgb = hsl2rgb(nick_int % 255, 70, nick_lightness);
+        rgb = rgb[2] | (rgb[1] << 8) | (rgb[0] << 16);
+        colour = '#' + rgb.toString(16);
+
+        ret = {color: colour};
+        ret.asCssString = function() {
+            return _.reduce(this, function(result, item, key){
+                return result + key + ':' + item + ';';
+            }, '');
+        };
+
+        return ret;
+    },
+
+
+    // Takes an IRC message object and parses it for displaying
+    generateMessageDisplayObj: function(msg) {
+        var nick_hex, time_difference,
+            message_words,
+            sb = this.model.get('scrollback'),
+            prev_msg = sb[sb.length-2],
+            hour, pm, am_pm_locale_key;
+
+        // Clone the msg object so we dont modify the original
+        msg = _.clone(msg);
+
+        // Defaults
+        msg.css_classes = '';
+        msg.nick_style = '';
+        msg.is_highlight = false;
+        msg.time_string = '';
+
+
+        // Nick highlight detecting
+        var nick = _kiwi.app.connections.active_connection.get('nick');
+        if ((new RegExp('(^|\\W)(' + escapeRegex(nick) + ')(\\W|$)', 'i')).test(msg.msg)) {
+            // Do not highlight the user's own input
+            if (msg.nick.localeCompare(nick) !== 0) {
+                msg.is_highlight = true;
+                msg.css_classes += ' highlight';
+            }
+        }
+
+        message_words = msg.msg.split(' ');
+        message_words = _.map(message_words, function(word) {
+            var parsed_word;
+
+            parsed_word = this.parseMessageUrls(word);
+            if (typeof parsed_word === 'string') return parsed_word;
+
+            parsed_word = this.parseMessageChannels(word);
+            if (typeof parsed_word === 'string') return parsed_word;
+
+            parsed_word = this.parseMessageNicks(word, (msg.type === 'privmsg'));
+            if (typeof parsed_word === 'string') return parsed_word;
+
+            parsed_word = _.escape(word);
+
+            // Replace text emoticons with images
+            if (_kiwi.global.settings.get('show_emoticons')) {
+                parsed_word = emoticonFromText(parsed_word);
+            }
+
+            return parsed_word;
+        }, this);
+
+        msg.unparsed_msg = msg.msg;
+        msg.msg = message_words.join(' ');
+
+        // Convert IRC formatting into HTML formatting
+        msg.msg = formatIRCMsg(msg.msg);
+
+        // Add some style to the nick
+        msg.nick_style = this.getNickStyles(msg.nick).asCssString();
+
+        // Generate a hex string from the nick to be used as a CSS class name
+        nick_hex = '';
+        if (msg.nick) {
+            _.map(msg.nick.split(''), function (char) {
+                nick_hex += char.charCodeAt(0).toString(16);
+            });
+            msg.css_classes += ' nick_' + nick_hex;
+        }
+
+        if (prev_msg) {
+            // Time difference between this message and the last (in minutes)
+            time_difference = (msg.time.getTime() - prev_msg.time.getTime())/1000/60;
+            if (prev_msg.nick === msg.nick && time_difference < 1) {
+                msg.css_classes += ' repeated_nick';
+            }
+        }
+
+        // Build up and add the line
+        if (_kiwi.global.settings.get('use_24_hour_timestamps')) {
+            msg.time_string = msg.time.getHours().toString().lpad(2, "0") + ":" + msg.time.getMinutes().toString().lpad(2, "0") + ":" + msg.time.getSeconds().toString().lpad(2, "0");
+        } else {
+            hour = msg.time.getHours();
+            pm = hour > 11;
+
+            hour = hour % 12;
+            if (hour === 0)
+                hour = 12;
+
+            am_pm_locale_key = pm ?
+                'client_views_panel_timestamp_pm' :
+                'client_views_panel_timestamp_am';
+
+            msg.time_string = translateText(am_pm_locale_key, hour + ":" + msg.time.getMinutes().toString().lpad(2, "0") + ":" + msg.time.getSeconds().toString().lpad(2, "0"));
+        }
+
+        return msg;
+    },
+
+
+    topic: function (topic) {
+        if (typeof topic !== 'string' || !topic) {
+            topic = this.model.get("topic");
+        }
+
+        this.model.addMsg('', styleText('channel_topic', {text: topic, channel: this.model.get('name')}), 'topic');
+
+        // If this is the active channel then update the topic bar
+        if (_kiwi.app.panels().active === this.model) {
+            _kiwi.app.topicbar.setCurrentTopicFromChannel(this.model);
+        }
+    },
+
+    topicSetBy: function (topic) {
+        // If this is the active channel then update the topic bar
+        if (_kiwi.app.panels().active === this.model) {
+            _kiwi.app.topicbar.setCurrentTopicFromChannel(this.model);
+        }
+    },
+
+    // Click on a nickname
+    nickClick: function (event) {
+        var $target = $(event.currentTarget),
+            nick,
+            members = this.model.get('members'),
+            member;
+
+        event.stopPropagation();
+
+        // Check this current element for a nick before resorting to the main message
+        // (eg. inline nicks has the nick on its own element within the message)
+        nick = $target.data('nick');
+        if (!nick) {
+            nick = $target.parent('.msg').data('message').nick;
+        }
+
+        // Make sure this nick is still in the channel
+        member = members ? members.getByNick(nick) : null;
+        if (!member) {
+            return;
+        }
+
+        _kiwi.global.events.emit('nick:select', {target: $target, member: member, source: 'message'})
+        .then(_.bind(this.openUserMenuForNick, this, $target, member));
+    },
+
+
+    updateLastSeenMarker: function() {
+        if (this.model.isActive()) {
+            // Remove the previous last seen classes
+            this.$(".last_seen").removeClass("last_seen");
+
+            // Mark the last message the user saw
+            this.$messages.children().last().addClass("last_seen");
+        }
+    },
+
+
+    openUserMenuForNick: function ($target, member) {
+        var members = this.model.get('members'),
+            are_we_an_op = !!members.getByNick(_kiwi.app.connections.active_connection.get('nick')).get('is_op'),
+            userbox, menubox;
+
+        userbox = new _kiwi.view.UserBox();
+        userbox.setTargets(member, this.model);
+        userbox.displayOpItems(are_we_an_op);
+
+        menubox = new _kiwi.view.MenuBox(member.get('nick') || 'User');
+        menubox.addItem('userbox', userbox.$el);
+        menubox.showFooter(false);
+
+        _kiwi.global.events.emit('usermenu:created', {menu: menubox, userbox: userbox, user: member})
+        .then(_.bind(function() {
+            menubox.show();
+
+            // Position the userbox + menubox
+            var target_offset = $target.offset(),
+                t = target_offset.top,
+                m_bottom = t + menubox.$el.outerHeight(),  // Where the bottom of menu will be
+                memberlist_bottom = this.$el.parent().offset().top + this.$el.parent().outerHeight();
+
+            // If the bottom of the userbox is going to be too low.. raise it
+            if (m_bottom > memberlist_bottom){
+                t = memberlist_bottom - menubox.$el.outerHeight();
+            }
+
+            // Set the new positon
+            menubox.$el.offset({
+                left: target_offset.left,
+                top: t
+            });
+        }, this))
+        .catch(_.bind(function() {
+            userbox = null;
+
+            menu.dispose();
+            menu = null;
+        }, this));
+    },
+
+
+    chanClick: function (event) {
+        var target = (event.target) ? $(event.target).data('channel') : $(event.srcElement).data('channel');
+
+        _kiwi.app.connections.active_connection.gateway.join(target);
+    },
+
+
+    mediaClick: function (event) {
+        var $media = $(event.target).parents('.media');
+        var media_message;
+
+        if ($media.data('media')) {
+            media_message = $media.data('media');
+        } else {
+            media_message = new _kiwi.view.MediaMessage({el: $media[0]});
+
+            // Cache this MediaMessage instance for when it's opened again
+            $media.data('media', media_message);
+        }
+
+        media_message.toggle();
+    },
+
+
+    // Cursor hovers over a message
+    msgEnter: function (event) {
+        var nick_class;
+
+        // Find a valid class that this element has
+        _.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) {
+            if (css_class.match(/^nick_[a-z0-9]+/i)) {
+                nick_class = css_class;
+            }
+        });
+
+        // If no class was found..
+        if (!nick_class) return;
+
+        $('.'+nick_class).addClass('global_nick_highlight');
+    },
+
+
+    // Cursor leaves message
+    msgLeave: function (event) {
+        var nick_class;
+
+        // Find a valid class that this element has
+        _.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) {
+            if (css_class.match(/^nick_[a-z0-9]+/i)) {
+                nick_class = css_class;
+            }
+        });
+
+        // If no class was found..
+        if (!nick_class) return;
+
+        $('.'+nick_class).removeClass('global_nick_highlight');
+    }
+});
+
+
+
+_kiwi.view.Applet = _kiwi.view.Panel.extend({
+    className: 'panel applet',
+    initialize: function (options) {
+        this.initializePanel(options);
+    }
+});
+
+
+_kiwi.view.Application = Backbone.View.extend({
+    initialize: function () {
+        var that = this;
+
+        this.$el = $($('#tmpl_application').html().trim());
+        this.el = this.$el[0];
+
+        $(this.model.get('container') || 'body').append(this.$el);
+
+        this.elements = {
+            panels:        this.$el.find('.panels'),
+            right_bar:     this.$el.find('.right_bar'),
+            toolbar:       this.$el.find('.toolbar'),
+            controlbox:    this.$el.find('.controlbox'),
+            resize_handle: this.$el.find('.memberlists_resize_handle')
+        };
+
+        $(window).resize(function() { that.doLayout.apply(that); });
+        this.elements.toolbar.resize(function() { that.doLayout.apply(that); });
+        this.elements.controlbox.resize(function() { that.doLayout.apply(that); });
+
+        // Change the theme when the config is changed
+        _kiwi.global.settings.on('change:theme', this.updateTheme, this);
+        this.updateTheme(getQueryVariable('theme'));
+
+        _kiwi.global.settings.on('change:channel_list_style', this.setTabLayout, this);
+        this.setTabLayout(_kiwi.global.settings.get('channel_list_style'));
+
+        _kiwi.global.settings.on('change:show_timestamps', this.displayTimestamps, this);
+        this.displayTimestamps(_kiwi.global.settings.get('show_timestamps'));
+
+        this.$el.appendTo($('body'));
+        this.doLayout();
+
+        $(document).keydown(this.setKeyFocus);
+
+        // Confirmation require to leave the page
+        window.onbeforeunload = function () {
+            if (_kiwi.gateway.isConnected()) {
+                return _kiwi.global.i18n.translate('client_views_application_close_notice').fetch();
+            }
+        };
+
+        // Keep tabs on the browser having focus
+        this.has_focus = true;
+
+        $(window).on('focus', function windowOnFocus() {
+            that.has_focus = true;
+        });
+
+        $(window).on('blur', function windowOnBlur() {
+            var active_panel = that.model.panels().active;
+            if (active_panel && active_panel.view.updateLastSeenMarker) {
+                active_panel.view.updateLastSeenMarker();
+            }
+
+            that.has_focus = false;
+        });
+
+        // If we get a touchstart event, make note of it so we know we're using a touchscreen
+        $(window).on('touchstart', function windowOnTouchstart() {
+            that.$el.addClass('touch');
+            $(window).off('touchstart', windowOnTouchstart);
+        });
+
+
+        this.favicon = new _kiwi.view.Favicon();
+        this.initSound();
+
+        this.monitorPanelFallback();
+    },
+
+
+
+    updateTheme: function (theme_name) {
+        // If called by the settings callback, get the correct new_value
+        if (theme_name === _kiwi.global.settings) {
+            theme_name = arguments[1];
+        }
+
+        // If we have no theme specified, get it from the settings
+        if (!theme_name) theme_name = _kiwi.global.settings.get('theme') || 'relaxed';
+
+        theme_name = theme_name.toLowerCase();
+
+        // Clear any current theme
+        $('[data-theme]:not([disabled])').each(function (idx, link) {
+            var $link = $(link);
+            $link.attr('rel', 'alternate ' + $link.attr('rel')).attr('disabled', true)[0].disabled = true;
+        });
+
+        // Apply the new theme
+        var link = $('[data-theme][title=' + theme_name + ']');
+        if (link.length > 0) {
+            link.attr('rel', 'stylesheet').attr('disabled', false)[0].disabled = false;
+        }
+
+        this.doLayout();
+    },
+
+
+    setTabLayout: function (layout_style) {
+        // If called by the settings callback, get the correct new_value
+        if (layout_style === _kiwi.global.settings) {
+            layout_style = arguments[1];
+        }
+
+        if (layout_style == 'list') {
+            this.$el.addClass('chanlist_treeview');
+        } else {
+            this.$el.removeClass('chanlist_treeview');
+        }
+
+        this.doLayout();
+    },
+
+
+    displayTimestamps: function (show_timestamps) {
+        // If called by the settings callback, get the correct new_value
+        if (show_timestamps === _kiwi.global.settings) {
+            show_timestamps = arguments[1];
+        }
+
+        if (show_timestamps) {
+            this.$el.addClass('timestamps');
+        } else {
+            this.$el.removeClass('timestamps');
+        }
+    },
+
+
+    // Globally shift focus to the command input box on a keypress
+    setKeyFocus: function (ev) {
+        // If we're copying text, don't shift focus
+        if (ev.ctrlKey || ev.altKey || ev.metaKey) {
+            return;
+        }
+
+        // If we're typing into an input box somewhere, ignore
+        if ((ev.target.tagName.toLowerCase() === 'input') || (ev.target.tagName.toLowerCase() === 'textarea') || $(ev.target).attr('contenteditable')) {
+            return;
+        }
+
+        $('#kiwi .controlbox .inp').focus();
+    },
+
+
+    doLayout: function () {
+        var $kiwi = this.$el;
+        var $panels = this.elements.panels;
+        var $right_bar = this.elements.right_bar;
+        var $toolbar = this.elements.toolbar;
+        var $controlbox = this.elements.controlbox;
+        var $resize_handle = this.elements.resize_handle;
+
+        if (!$kiwi.is(':visible')) {
+            return;
+        }
+
+        var css_heights = {
+            top: $toolbar.outerHeight(true),
+            bottom: $controlbox.outerHeight(true)
+        };
+
+
+        // If any elements are not visible, full size the panals instead
+        if (!$toolbar.is(':visible')) {
+            css_heights.top = 0;
+        }
+
+        if (!$controlbox.is(':visible')) {
+            css_heights.bottom = 0;
+        }
+
+        // Apply the CSS sizes
+        $panels.css(css_heights);
+        $right_bar.css(css_heights);
+        $resize_handle.css(css_heights);
+
+        // If we have channel tabs on the side, adjust the height
+        if ($kiwi.hasClass('chanlist_treeview')) {
+            this.$el.find('.tabs', $kiwi).css(css_heights);
+        }
+
+        // Determine if we have a narrow window (mobile/tablet/or even small desktop window)
+        if ($kiwi.outerWidth() < 420) {
+            $kiwi.addClass('narrow');
+            if (this.model.rightbar && this.model.rightbar.keep_hidden !== true)
+                this.model.rightbar.toggle(true);
+        } else {
+            $kiwi.removeClass('narrow');
+            if (this.model.rightbar && this.model.rightbar.keep_hidden !== false)
+                this.model.rightbar.toggle(false);
+        }
+
+        // Set the panels width depending on the memberlist visibility
+        if (!$right_bar.hasClass('disabled')) {
+            // Panels to the side of the memberlist
+            $panels.css('right', $right_bar.outerWidth(true));
+            // The resize handle sits overlapping the panels and memberlist
+            $resize_handle.css('left', $right_bar.position().left - ($resize_handle.outerWidth(true) / 2));
+        } else {
+            // Memberlist is hidden so panels to the right edge
+            $panels.css('right', 0);
+            // And move the handle just out of sight to the right
+            $resize_handle.css('left', $panels.outerWidth(true));
+        }
+
+        var input_wrap_width = parseInt($controlbox.find('.input_tools').outerWidth(), 10);
+        $controlbox.find('.input_wrap').css('right', input_wrap_width + 7);
+    },
+
+
+    alertWindow: function (title) {
+        if (!this.alertWindowTimer) {
+            this.alertWindowTimer = new (function () {
+                var that = this;
+                var tmr;
+                var has_focus = true;
+                var state = 0;
+                var default_title = _kiwi.app.server_settings.client.window_title || 'Kiwi IRC';
+                var title = 'Kiwi IRC';
+
+                this.setTitle = function (new_title) {
+                    new_title = new_title || default_title;
+                    window.document.title = new_title;
+                    return new_title;
+                };
+
+                this.start = function (new_title) {
+                    // Don't alert if we already have focus
+                    if (has_focus) return;
+
+                    title = new_title;
+                    if (tmr) return;
+                    tmr = setInterval(this.update, 1000);
+                };
+
+                this.stop = function () {
+                    // Stop the timer and clear the title
+                    if (tmr) clearInterval(tmr);
+                    tmr = null;
+                    this.setTitle();
+
+                    // Some browsers don't always update the last title correctly
+                    // Wait a few seconds and then reset
+                    setTimeout(this.reset, 2000);
+                };
+
+                this.reset = function () {
+                    if (tmr) return;
+                    that.setTitle();
+                };
+
+
+                this.update = function () {
+                    if (state === 0) {
+                        that.setTitle(title);
+                        state = 1;
+                    } else {
+                        that.setTitle();
+                        state = 0;
+                    }
+                };
+
+                $(window).focus(function (event) {
+                    has_focus = true;
+                    that.stop();
+
+                    // Some browsers don't always update the last title correctly
+                    // Wait a few seconds and then reset
+                    setTimeout(that.reset, 2000);
+                });
+
+                $(window).blur(function (event) {
+                    has_focus = false;
+                });
+            })();
+        }
+
+        this.alertWindowTimer.start(title);
+    },
+
+
+    barsHide: function (instant) {
+        var that = this;
+
+        if (!instant) {
+            this.$el.find('.toolbar').slideUp({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
+            $('#kiwi .controlbox').slideUp({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
+        } else {
+            this.$el.find('.toolbar').slideUp(0);
+            $('#kiwi .controlbox').slideUp(0);
+            this.doLayout();
+        }
+    },
+
+    barsShow: function (instant) {
+        var that = this;
+
+        if (!instant) {
+            this.$el.find('.toolbar').slideDown({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
+            $('#kiwi .controlbox').slideDown({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
+        } else {
+            this.$el.find('.toolbar').slideDown(0);
+            $('#kiwi .controlbox').slideDown(0);
+            this.doLayout();
+        }
+    },
+
+
+    initSound: function () {
+        var that = this,
+            base_path = this.model.get('base_path');
+
+        $script(base_path + '/assets/libs/soundmanager2/soundmanager2-nodebug-jsmin.js', function() {
+            if (typeof soundManager === 'undefined')
+                return;
+
+            soundManager.setup({
+                url: base_path + '/assets/libs/soundmanager2/',
+                flashVersion: 9, // optional: shiny features (default = 8)// optional: ignore Flash where possible, use 100% HTML5 mode
+                preferFlash: true,
+
+                onready: function() {
+                    that.sound_object = soundManager.createSound({
+                        id: 'highlight',
+                        url: base_path + '/assets/sound/highlight.mp3'
+                    });
+                }
+            });
+        });
+    },
+
+
+    playSound: function (sound_id) {
+        if (!this.sound_object) return;
+
+        if (_kiwi.global.settings.get('mute_sounds'))
+            return;
+
+        soundManager.play(sound_id);
+    },
+
+
+    showNotification: function(title, message) {
+        var icon = this.model.get('base_path') + '/assets/img/ico.png',
+            notifications = _kiwi.utils.notifications;
+
+        if (!this.has_focus && notifications.allowed()) {
+            notifications
+                .create(title, { icon: icon, body: message })
+                .closeAfter(5000)
+                .on('click', _.bind(window.focus, window));
+        }
+    },
+
+    monitorPanelFallback: function() {
+        var panel_access = [];
+
+        this.model.panels.on('active', function() {
+            var panel = _kiwi.app.panels().active,
+                panel_index;
+
+            // If the panel is already open, remove it so we can put it back in first place
+            panel_index = _.indexOf(panel_access, panel.cid);
+
+            if (panel_index > -1) {
+                panel_access.splice(panel_index, 1);
+            }
+
+            //Make this panel the most recently accessed
+            panel_access.unshift(panel.cid);
+        });
+
+        this.model.panels.on('remove', function(panel) {
+            // If closing the active panel, switch to the last-accessed panel
+            if (panel_access[0] === panel.cid) {
+                panel_access.shift();
+
+                //Get the last-accessed panel model now that we removed the closed one
+                var model = _.find(_kiwi.app.panels('applets').concat(_kiwi.app.panels('connections')), {cid: panel_access[0]});
+
+                if (model) {
+                    model.view.show();
+                }
+            }
+        });
+    }
+});
+
+
+
+_kiwi.view.AppToolbar = Backbone.View.extend({
+    events: {
+        'click .settings': 'clickSettings',
+        'click .startup': 'clickStartup'
+    },
+
+    initialize: function () {
+        // Remove the new connection/startup link if the server has disabled server changing
+        if (_kiwi.app.server_settings.connection && !_kiwi.app.server_settings.connection.allow_change) {
+            this.$('.startup').css('display', 'none');
+        }
+    },
+
+    clickSettings: function (event) {
+        event.preventDefault();
+        _kiwi.app.controlbox.processInput('/settings');
+    },
+
+    clickStartup: function (event) {
+        event.preventDefault();
+        _kiwi.app.startup_applet.view.show();
+    }
+});
+
+
+
+_kiwi.view.ControlBox = Backbone.View.extend({
+    events: {
+        'keydown .inp': 'process',
+        'click .nick': 'showNickChange'
+    },
+
+    initialize: function () {
+        var that = this;
+
+        this.buffer = [];  // Stores previously run commands
+        this.buffer_pos = 0;  // The current position in the buffer
+
+        this.preprocessor = new InputPreProcessor();
+        this.preprocessor.recursive_depth = 5;
+
+        // Hold tab autocomplete data
+        this.tabcomplete = {active: false, data: [], prefix: ''};
+
+        // Keep the nick view updated with nick changes
+        _kiwi.app.connections.on('change:nick', function(connection) {
+            // Only update the nick view if it's the active connection
+            if (connection !== _kiwi.app.connections.active_connection)
+                return;
+
+            $('.nick', that.$el).text(connection.get('nick'));
+        });
+
+        // Update our nick view as we flick between connections
+        _kiwi.app.connections.on('active', function(panel, connection) {
+            $('.nick', that.$el).text(connection.get('nick'));
+        });
+
+        // Keep focus on the input box as we flick between panels
+        _kiwi.app.panels.bind('active', function (active_panel) {
+            if (active_panel.isChannel() || active_panel.isServer() || active_panel.isQuery()) {
+                that.$('.inp').focus();
+            }
+        });
+    },
+
+    render: function() {
+        var send_message_text = translateText('client_views_controlbox_message');
+        this.$('.inp').attr('placeholder', send_message_text);
+
+        return this;
+    },
+
+    showNickChange: function (ev) {
+        // Nick box already open? Don't do it again
+        if (this.nick_change)
+            return;
+
+        this.nick_change = new _kiwi.view.NickChangeBox();
+        this.nick_change.render();
+
+        this.listenTo(this.nick_change, 'close', function() {
+            delete this.nick_change;
+        });
+    },
+
+    process: function (ev) {
+        var that = this,
+            inp = $(ev.currentTarget),
+            inp_val = inp.val(),
+            meta;
+
+        if (navigator.appVersion.indexOf("Mac") !== -1) {
+            meta = ev.metaKey;
+        } else {
+            meta = ev.altKey;
+        }
+
+        // If not a tab key, reset the tabcomplete data
+        if (this.tabcomplete.active && ev.keyCode !== 9) {
+            this.tabcomplete.active = false;
+            this.tabcomplete.data = [];
+            this.tabcomplete.prefix = '';
+        }
+
+        switch (true) {
+        case (ev.keyCode === 13):              // return
+            inp_val = inp_val.trim();
+
+            if (inp_val) {
+                $.each(inp_val.split('\n'), function (idx, line) {
+                    that.processInput(line);
+                });
+
+                this.buffer.push(inp_val);
+                this.buffer_pos = this.buffer.length;
+            }
+
+            inp.val('');
+            return false;
+
+            break;
+
+        case (ev.keyCode === 38):              // up
+            if (this.buffer_pos > 0) {
+                this.buffer_pos--;
+                inp.val(this.buffer[this.buffer_pos]);
+            }
+            //suppress browsers default behavior as it would set the cursor at the beginning
+            return false;
+
+        case (ev.keyCode === 40):              // down
+            if (this.buffer_pos < this.buffer.length) {
+                this.buffer_pos++;
+                inp.val(this.buffer[this.buffer_pos]);
+            }
+            break;
+
+        case (ev.keyCode === 219 && meta):            // [ + meta
+            // Find all the tab elements and get the index of the active tab
+            var $tabs = $('#kiwi .tabs').find('li[class!=connection]');
+            var cur_tab_ind = (function() {
+                for (var idx=0; idx<$tabs.length; idx++){
+                    if ($($tabs[idx]).hasClass('active'))
+                        return idx;
+                }
+            })();
+
+            // Work out the previous tab along. Wrap around if needed
+            if (cur_tab_ind === 0) {
+                $prev_tab = $($tabs[$tabs.length - 1]);
+            } else {
+                $prev_tab = $($tabs[cur_tab_ind - 1]);
+            }
+
+            $prev_tab.click();
+            return false;
+
+        case (ev.keyCode === 221 && meta):            // ] + meta
+            // Find all the tab elements and get the index of the active tab
+            var $tabs = $('#kiwi .tabs').find('li[class!=connection]');
+            var cur_tab_ind = (function() {
+                for (var idx=0; idx<$tabs.length; idx++){
+                    if ($($tabs[idx]).hasClass('active'))
+                        return idx;
+                }
+            })();
+
+            // Work out the next tab along. Wrap around if needed
+            if (cur_tab_ind === $tabs.length - 1) {
+                $next_tab = $($tabs[0]);
+            } else {
+                $next_tab = $($tabs[cur_tab_ind + 1]);
+            }
+
+            $next_tab.click();
+            return false;
+
+        case (ev.keyCode === 9     //Check if ONLY tab is pressed
+            && !ev.shiftKey        //(user could be using some browser
+            && !ev.altKey          //keyboard shortcut)
+            && !ev.metaKey
+            && !ev.ctrlKey):
+            this.tabcomplete.active = true;
+            if (_.isEqual(this.tabcomplete.data, [])) {
+                // Get possible autocompletions
+                var ac_data = [],
+                    members = _kiwi.app.panels().active.get('members');
+
+                // If we have a members list, get the models. Otherwise empty array
+                members = members ? members.models : [];
+
+                $.each(members, function (i, member) {
+                    if (!member) return;
+                    ac_data.push(member.get('nick'));
+                });
+
+                ac_data.push(_kiwi.app.panels().active.get('name'));
+
+                ac_data = _.sortBy(ac_data, function (nick) {
+                    return nick.toLowerCase();
+                });
+                this.tabcomplete.data = ac_data;
+            }
+
+            if (inp_val[inp[0].selectionStart - 1] === ' ') {
+                return false;
+            }
+
+            (function () {
+                var tokens,              // Words before the cursor position
+                    val,                 // New value being built up
+                    p1,                  // Position in the value just before the nick
+                    newnick,             // New nick to be displayed (cycles through)
+                    range,               // TextRange for setting new text cursor position
+                    nick,                // Current nick in the value
+                    trailing = ': ';     // Text to be inserted after a tabbed nick
+
+                tokens = inp_val.substring(0, inp[0].selectionStart).split(' ');
+                if (tokens[tokens.length-1] == ':')
+                    tokens.pop();
+
+                // Only add the trailing text if not at the beginning of the line
+                if (tokens.length > 1)
+                    trailing = '';
+
+                nick  = tokens[tokens.length - 1];
+
+                if (this.tabcomplete.prefix === '') {
+                    this.tabcomplete.prefix = nick;
+                }
+
+                this.tabcomplete.data = _.select(this.tabcomplete.data, function (n) {
+                    return (n.toLowerCase().indexOf(that.tabcomplete.prefix.toLowerCase()) === 0);
+                });
+
+                if (this.tabcomplete.data.length > 0) {
+                    // Get the current value before cursor position
+                    p1 = inp[0].selectionStart - (nick.length);
+                    val = inp_val.substr(0, p1);
+
+                    // Include the current selected nick
+                    newnick = this.tabcomplete.data.shift();
+                    this.tabcomplete.data.push(newnick);
+                    val += newnick;
+
+                    if (inp_val.substr(inp[0].selectionStart, 2) !== trailing)
+                        val += trailing;
+
+                    // Now include the rest of the current value
+                    val += inp_val.substr(inp[0].selectionStart);
+
+                    inp.val(val);
+
+                    // Move the cursor position to the end of the nick
+                    if (inp[0].setSelectionRange) {
+                        inp[0].setSelectionRange(p1 + newnick.length + trailing.length, p1 + newnick.length + trailing.length);
+                    } else if (inp[0].createTextRange) { // not sure if this bit is actually needed....
+                        range = inp[0].createTextRange();
+                        range.collapse(true);
+                        range.moveEnd('character', p1 + newnick.length + trailing.length);
+                        range.moveStart('character', p1 + newnick.length + trailing.length);
+                        range.select();
+                    }
+                }
+            }).apply(this);
+            return false;
+        }
+    },
+
+
+    processInput: function (command_raw) {
+        var that = this,
+            command, params, events_data,
+            pre_processed;
+
+        // If sending a message when not in a channel or query window, automatically
+        // convert it into a command
+        if (command_raw[0] !== '/' && !_kiwi.app.panels().active.isChannel() && !_kiwi.app.panels().active.isQuery()) {
+            command_raw = '/' + command_raw;
+        }
+
+        // The default command
+        if (command_raw[0] !== '/' || command_raw.substr(0, 2) === '//') {
+            // Remove any slash escaping at the start (ie. //)
+            command_raw = command_raw.replace(/^\/\//, '/');
+
+            // Prepend the default command
+            command_raw = '/msg ' + _kiwi.app.panels().active.get('name') + ' ' + command_raw;
+        }
+
+        // Process the raw command for any aliases
+        this.preprocessor.vars.server = _kiwi.app.connections.active_connection.get('name');
+        this.preprocessor.vars.channel = _kiwi.app.panels().active.get('name');
+        this.preprocessor.vars.destination = this.preprocessor.vars.channel;
+        command_raw = this.preprocessor.process(command_raw);
+
+        // Extract the command and parameters
+        params = command_raw.split(/\s/);
+        if (params[0][0] === '/') {
+            command = params[0].substr(1).toLowerCase();
+            params = params.splice(1, params.length - 1);
+        } else {
+            // Default command
+            command = 'msg';
+            params.unshift(_kiwi.app.panels().active.get('name'));
+        }
+
+        // Emit a plugin event for any modifications
+        events_data = {command: command, params: params};
+
+        _kiwi.global.events.emit('command', events_data)
+        .then(function() {
+            // Trigger the command events
+            that.trigger('command', {command: events_data.command, params: events_data.params});
+            that.trigger('command:' + events_data.command, {command: events_data.command, params: events_data.params});
+
+            // If we didn't have any listeners for this event, fire a special case
+            // TODO: This feels dirty. Should this really be done..?
+            if (!that._events['command:' + events_data.command]) {
+                that.trigger('unknown_command', {command: events_data.command, params: events_data.params});
+            }
+        });
+    },
+
+
+    addPluginIcon: function ($icon) {
+        var $tool = $('<div class="tool"></div>').append($icon);
+        this.$el.find('.input_tools').append($tool);
+        _kiwi.app.view.doLayout();
+    }
+});
+
+
+
+_kiwi.view.Favicon = Backbone.View.extend({
+    initialize: function () {
+        var that = this,
+            $win = $(window);
+
+        this.has_focus = true;
+        this.highlight_count = 0;
+        // Check for html5 canvas support
+        this.has_canvas_support = !!window.CanvasRenderingContext2D;
+
+        // Store the original favicon
+        this.original_favicon = $('link[rel~="icon"]')[0].href;
+
+        // Create our favicon canvas
+        this._createCanvas();
+
+        // Reset favicon notifications when user focuses window
+        $win.on('focus', function () {
+            that.has_focus = true;
+            that._resetHighlights();
+        });
+        $win.on('blur', function () {
+            that.has_focus = false;
+        });
+    },
+
+    newHighlight: function () {
+        var that = this;
+        if (!this.has_focus) {
+            this.highlight_count++;
+            if (this.has_canvas_support) {
+                this._drawFavicon(function() {
+                    that._drawBubble(that.highlight_count.toString());
+                    that._refreshFavicon(that.canvas.toDataURL());
+                });
+            }
+        }
+    },
+
+    _resetHighlights: function () {
+        var that = this;
+        this.highlight_count = 0;
+        this._refreshFavicon(this.original_favicon);
+    },
+
+    _drawFavicon: function (callback) {
+        var that = this,
+            canvas = this.canvas,
+            context = canvas.getContext('2d'),
+            favicon_image = new Image();
+
+        // Allow cross origin resource requests
+        favicon_image.crossOrigin = 'anonymous';
+        // Trigger the load event
+        favicon_image.src = this.original_favicon;
+
+        favicon_image.onload = function() {
+            // Clear canvas from prevous iteration
+            context.clearRect(0, 0, canvas.width, canvas.height);
+            // Draw the favicon itself
+            context.drawImage(favicon_image, 0, 0, canvas.width, canvas.height);
+            callback();
+        };
+    },
+
+    _drawBubble: function (label) {
+        var letter_spacing,
+            bubble_width = 0, bubble_height = 0,
+            canvas = this.canvas,
+            context = test_context = canvas.getContext('2d'),
+            canvas_width = canvas.width,
+            canvas_height = canvas.height;
+
+        // Different letter spacing for MacOS 
+        if (navigator.appVersion.indexOf("Mac") !== -1) {
+            letter_spacing = -1.5;
+        }
+        else {
+            letter_spacing = -1;
+        }
+
+        // Setup a test canvas to get text width
+        test_context.font = context.font = 'bold 10px Arial';
+        test_context.textAlign = 'right';
+        this._renderText(test_context, label, 0, 0, letter_spacing);
+
+        // Calculate bubble width based on letter spacing and padding
+        bubble_width = test_context.measureText(label).width + letter_spacing * (label.length - 1) + 2;
+        // Canvas does not have any way of measuring text height, so we just do it manually and add 1px top/bottom padding
+        bubble_height = 9;
+
+        // Set bubble coordinates
+        bubbleX = canvas_width - bubble_width;
+        bubbleY = canvas_height - bubble_height;
+
+        // Draw bubble background
+        context.fillStyle = 'red';
+        context.fillRect(bubbleX, bubbleY, bubble_width, bubble_height);
+
+        // Draw the text
+        context.fillStyle = 'white';
+        this._renderText(context, label, canvas_width - 1, canvas_height - 1, letter_spacing);
+    },
+
+    _refreshFavicon: function (url) {
+        $('link[rel~="icon"]').remove();
+        $('<link rel="shortcut icon" href="' + url + '">').appendTo($('head'));
+    },
+
+    _createCanvas: function () {
+        var canvas = document.createElement('canvas');
+            canvas.width = 16;
+            canvas.height = 16;
+        
+        this.canvas = canvas;
+    },
+
+    _renderText: function (context, text, x, y, letter_spacing) {
+        // A hacky solution for letter-spacing, but works well with small favicon text
+        // Modified from http://jsfiddle.net/davidhong/hKbJ4/
+        var current,
+            characters = text.split('').reverse(),
+            index = 0,
+            currentPosition = x;
+
+        while (index < text.length) {
+            current = characters[index++];
+            context.fillText(current, currentPosition, y);
+            currentPosition += (-1 * (context.measureText(current).width + letter_spacing));
+        }
+
+        return context;
+    }
+});
+
+
+
+_kiwi.view.MediaMessage = Backbone.View.extend({
+    events: {
+        'click .media_close': 'close'
+    },
+
+    initialize: function () {
+        // Get the URL from the data
+        this.url = this.$el.data('url');
+    },
+
+    toggle: function () {
+        if (!this.$content || !this.$content.is(':visible')) {
+            this.open();
+        } else {
+            this.close();
+        }
+    },
+
+    // Close the media content and remove it from display
+    close: function () {
+        var that = this;
+        this.$content.slideUp('fast', function () {
+            that.$content.remove();
+        });
+    },
+
+    // Open the media content within its wrapper
+    open: function () {
+        // Create the content div if we haven't already
+        if (!this.$content) {
+            this.$content = $('<div class="media_content"><a class="media_close"><i class="fa fa-chevron-up"></i> ' + _kiwi.global.i18n.translate('client_views_mediamessage_close').fetch() + '</a><br /><div class="content"></div></div>');
+            this.$content.find('.content').append(this.mediaTypes[this.$el.data('type')].apply(this, []) || _kiwi.global.i18n.translate('client_views_mediamessage_notfound').fetch() + ' :(');
+        }
+
+        // Now show the content if not already
+        if (!this.$content.is(':visible')) {
+            // Hide it first so the slideDown always plays
+            this.$content.hide();
+
+            // Add the media content and slide it into view
+            this.$el.append(this.$content);
+            this.$content.slideDown();
+        }
+    },
+
+
+
+    // Generate the media content for each recognised type
+    mediaTypes: {
+        twitter: function () {
+            var tweet_id = this.$el.data('tweetid');
+            var that = this;
+
+            $.getJSON('https://api.twitter.com/1/statuses/oembed.json?id=' + tweet_id + '&callback=?', function (data) {
+                that.$content.find('.content').html(data.html);
+            });
+
+            return $('<div>' + _kiwi.global.i18n.translate('client_views_mediamessage_load_tweet').fetch() + '...</div>');
+        },
+
+
+        image: function () {
+            return $('<a href="' + this.url + '" target="_blank"><img height="100" src="' + this.url + '" /></a>');
+        },
+
+
+        imgur: function () {
+            var that = this;
+
+            $.getJSON('http://api.imgur.com/oembed?url=' + this.url, function (data) {
+                var img_html = '<a href="' + data.url + '" target="_blank"><img height="100" src="' + data.url + '" /></a>';
+                that.$content.find('.content').html(img_html);
+            });
+
+            return $('<div>' + _kiwi.global.i18n.translate('client_views_mediamessage_load_image').fetch() + '...</div>');
+        },
+
+
+        reddit: function () {
+            var that = this;
+            var matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(this.url);
+
+            $.getJSON('http://www.' + matches[0] + '.json?jsonp=?', function (data) {
+                console.log('Loaded reddit data', data);
+                var post = data[0].data.children[0].data;
+                var thumb = '';
+
+                // Show a thumbnail if there is one
+                if (post.thumbnail) {
+                    //post.thumbnail = 'http://www.eurotunnel.com/uploadedImages/commercial/back-steps-icon-arrow.png';
+
+                    // Hide the thumbnail if an over_18 image
+                    if (post.over_18) {
+                        thumb = '<span class="thumbnail_nsfw" onclick="$(this).find(\'p\').remove(); $(this).find(\'img\').css(\'visibility\', \'visible\');">';
+                        thumb += '<p style="font-size:0.9em;line-height:1.2em;cursor:pointer;">Show<br />NSFW</p>';
+                        thumb += '<img src="' + post.thumbnail + '" class="thumbnail" style="visibility:hidden;" />';
+                        thumb += '</span>';
+                    } else {
+                        thumb = '<img src="' + post.thumbnail + '" class="thumbnail" />';
+                    }
+                }
+
+                // Build the template string up
+                var tmpl = '<div>' + thumb + '<b><%- title %></b><br />Posted by <%- author %>. &nbsp;&nbsp; ';
+                tmpl += '<i class="fa fa-arrow-up"></i> <%- ups %> &nbsp;&nbsp; <i class="fa fa-arrow-down"></i> <%- downs %><br />';
+                tmpl += '<%- num_comments %> comments made. <a href="http://www.reddit.com<%- permalink %>">View post</a></div>';
+
+                that.$content.find('.content').html(_.template(tmpl, post));
+            });
+
+            return $('<div>' + _kiwi.global.i18n.translate('client_views_mediamessage_load_reddit').fetch() + '...</div>');
+        },
+
+
+        youtube: function () {
+            var ytid = this.$el.data('ytid');
+            var that = this;
+            var yt_html = '<iframe width="480" height="270" src="https://www.youtube.com/embed/'+ ytid +'?feature=oembed" frameborder="0" allowfullscreen=""></iframe>';
+            that.$content.find('.content').html(yt_html);
+
+            return $('');
+        },
+
+
+        gist: function () {
+            var that = this,
+                matches = (/https?:\/\/gist\.github\.com\/(?:[a-z0-9-]*\/)?([a-z0-9]+)(\#(.+))?$/i).exec(this.url);
+
+            $.getJSON('https://gist.github.com/'+matches[1]+'.json?callback=?' + (matches[2] || ''), function (data) {
+                $('body').append('<link rel="stylesheet" href="' + data.stylesheet + '" type="text/css" />');
+                that.$content.find('.content').html(data.div);
+            });
+
+            return $('<div>' + _kiwi.global.i18n.translate('client_views_mediamessage_load_gist').fetch() + '...</div>');
+        },
+
+        spotify: function () {
+            var uri = this.$el.data('uri'),
+                method = this.$el.data('method'),
+                spot, html;
+
+            switch (method) {
+                case "track":
+                case "album":
+                    spot = {
+                        url: 'https://embed.spotify.com/?uri=' + uri,
+                        width: 300,
+                        height: 80
+                    };
+                    break;
+                case "artist":
+                    spot = {
+                        url: 'https://embed.spotify.com/follow/1/?uri=' + uri +'&size=detail&theme=dark',
+                        width: 300,
+                        height: 56
+                    };
+                    break;
+            }
+
+            html = '<iframe src="' + spot.url + '" width="' + spot.width + '" height="' + spot.height + '" frameborder="0" allowtransparency="true"></iframe>';
+
+            return $(html);
+        },
+
+        soundcloud: function () {
+            var url = this.$el.data('url'),
+                $content = $('<div></div>').text(_kiwi.global.i18n.translate('client_models_applet_loading').fetch());
+
+            $.getJSON('https://soundcloud.com/oembed', { url: url })
+                .then(function (data) {
+                    $content.empty().append(
+                        $(data.html).attr('height', data.height - 100)
+                    );
+                }, function () {
+                    $content.text(_kiwi.global.i18n.translate('client_views_mediamessage_notfound').fetch());
+                });
+
+            return $content;
+        },
+
+        custom: function() {
+            var type = this.constructor.types[this.$el.data('index')];
+
+            if (!type)
+                return;
+
+            return $(type.buildHtml(this.$el.data('url')));
+        }
+
+    }
+    }, {
+
+    /**
+     * Add a media message type to append HTML after a matching URL
+     * match() should return a truthy value if it wants to handle this URL
+     * buildHtml() should return the HTML string to be used within the drop down
+     */
+    addType: function(match, buildHtml) {
+        if (typeof match !== 'function' || typeof buildHtml !== 'function')
+            return;
+
+        this.types = this.types || [];
+        this.types.push({match: match, buildHtml: buildHtml});
+    },
+
+
+    // Build the closed media HTML from a URL
+    buildHtml: function (url) {
+        var html = '', matches;
+
+        _.each(this.types || [], function(type, type_idx) {
+            if (!type.match(url))
+                return;
+
+            // Add which media type should handle this media message. Will be read when it's clicked on
+            html += '<span class="media" title="Open" data-type="custom" data-index="'+type_idx+'" data-url="' + _.escape(url) + '"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
+        });
+
+        // Is it an image?
+        if (url.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)) {
+            html += '<span class="media image" data-type="image" data-url="' + url + '" title="Open Image"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
+        }
+
+        // Is this an imgur link not picked up by the images regex?
+        matches = (/imgur\.com\/[^/]*(?!=\.[^!.]+($|\?))/ig).exec(url);
+        if (matches && !url.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)) {
+            html += '<span class="media imgur" data-type="imgur" data-url="' + url + '" title="Open Image"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
+        }
+
+        // Is it a tweet?
+        matches = (/https?:\/\/twitter.com\/([a-zA-Z0-9_]+)\/status\/([0-9]+)/ig).exec(url);
+        if (matches) {
+            html += '<span class="media twitter" data-type="twitter" data-url="' + url + '" data-tweetid="' + matches[2] + '" title="Show tweet information"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
+        }
+
+        // Is reddit?
+        matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(url);
+        if (matches) {
+            html += '<span class="media reddit" data-type="reddit" data-url="' + url + '" title="Reddit thread"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
+        }
+
+        // Is youtube?
+        matches = (/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/gi).exec(url);
+        if (matches) {
+            html += '<span class="media youtube" data-type="youtube" data-url="' + url + '" data-ytid="' + matches[1] + '" title="YouTube Video"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
+        }
+
+        // Is a github gist?
+        matches = (/https?:\/\/gist\.github\.com\/(?:[a-z0-9-]*\/)?([a-z0-9]+)(\#(.+))?$/i).exec(url);
+        if (matches) {
+            html += '<span class="media gist" data-type="gist" data-url="' + url + '" data-gist_id="' + matches[1] + '" title="GitHub Gist"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
+        }
+
+        // Is this a spotify link?
+        matches = (/http:\/\/(?:play|open\.)?spotify.com\/(album|track|artist)\/([a-zA-Z0-9]+)\/?/i).exec(url);
+        if (matches) {
+            // Make it a Spotify URI! (spotify:<type>:<id>)
+            var method = matches[1],
+                uri = "spotify:" + matches[1] + ":" + matches[2];
+            html += '<span class="media spotify" data-type="spotify" data-uri="' + uri + '" data-method="' + method + '" title="Spotify ' + method + '"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
+        }
+
+        matches = (/(?:m\.)?(soundcloud\.com(?:\/.+))/i).exec(url);
+        if (matches) {
+            html += '<span class="media soundcloud" data-type="soundcloud" data-url="http://' + matches[1] + '" title="SoundCloud player"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
+        }
+
+        return html;
+    }
+});
+
+
+
+_kiwi.view.Member = Backbone.View.extend({
+    tagName: "li",
+    initialize: function (options) {
+        this.model.bind('change', this.render, this);
+        this.render();
+    },
+    render: function () {
+        var $this = this.$el,
+            prefix_css_class = (this.model.get('modes') || []).join(' ');
+
+        $this.attr('class', 'mode ' + prefix_css_class);
+        $this.html('<a class="nick"><span class="prefix">' + this.model.get("prefix") + '</span>' + this.model.get("nick") + '</a>');
+
+        return this;
+    }
+});
+
+
+_kiwi.view.MemberList = Backbone.View.extend({
+    tagName: "div",
+    events: {
+        "click .nick": "nickClick",
+        "click .channel_info": "channelInfoClick"
+    },
+
+    initialize: function (options) {
+        this.model.bind('all', this.render, this);
+        this.$el.appendTo('#kiwi .memberlists');
+
+        // Holds meta data. User counts, etc
+        this.$meta = $('<div class="meta"></div>').appendTo(this.$el);
+
+        // The list for holding the nicks
+        this.$list = $('<ul></ul>').appendTo(this.$el);
+    },
+    render: function () {
+        var that = this;
+
+        this.$list.empty();
+        this.model.forEach(function (member) {
+            member.view.$el.data('member', member);
+            that.$list.append(member.view.$el);
+        });
+
+        // User count
+        if(this.model.channel.isActive()) {
+            this.renderMeta();
+        }
+
+        return this;
+    },
+
+    renderMeta: function() {
+        var members_count = this.model.length + ' ' + translateText('client_applets_chanlist_users');
+        this.$meta.text(members_count);
+    },
+
+    nickClick: function (event) {
+        var $target = $(event.currentTarget).parent('li'),
+            member = $target.data('member');
+
+        _kiwi.global.events.emit('nick:select', {target: $target, member: member, source: 'nicklist'})
+        .then(_.bind(this.openUserMenuForItem, this, $target));
+    },
+
+
+    // Open a user menu for the given userlist item (<li>)
+    openUserMenuForItem: function($target) {
+        var member = $target.data('member'),
+            userbox,
+            are_we_an_op = !!this.model.getByNick(_kiwi.app.connections.active_connection.get('nick')).get('is_op');
+
+        userbox = new _kiwi.view.UserBox();
+        userbox.setTargets(member, this.model.channel);
+        userbox.displayOpItems(are_we_an_op);
+
+        var menu = new _kiwi.view.MenuBox(member.get('nick') || 'User');
+        menu.addItem('userbox', userbox.$el);
+        menu.showFooter(false);
+
+        _kiwi.global.events.emit('usermenu:created', {menu: menu, userbox: userbox, user: member})
+        .then(_.bind(function() {
+            menu.show();
+
+            var target_offset = $target.offset(),
+                t = target_offset.top,
+                m_bottom = t + menu.$el.outerHeight(),  // Where the bottom of menu will be
+                memberlist_bottom = this.$el.parent().offset().top + this.$el.parent().outerHeight(),
+                l = target_offset.left,
+                m_right = l + menu.$el.outerWidth(),  // Where the left of menu will be
+                memberlist_right = this.$el.parent().offset().left + this.$el.parent().outerWidth();
+
+            // If the bottom of the userbox is going to be too low.. raise it
+            if (m_bottom > memberlist_bottom){
+                t = memberlist_bottom - menu.$el.outerHeight();
+            }
+
+            // If the top of the userbox is going to be too high.. lower it
+            if (t < 0){
+                t = 0;
+            }
+
+            // If the right of the userbox is going off screen.. bring it in
+            if (m_right > memberlist_right){
+                l = memberlist_right - menu.$el.outerWidth();
+            }
+
+            // Set the new positon
+            menu.$el.offset({
+                left: l,
+                top: t
+            });
+
+        }, this))
+        .catch(_.bind(function() {
+            userbox = null;
+
+            menu.dispose();
+            menu = null;
+        }, this));
+    },
+
+
+    channelInfoClick: function(event) {
+        new _kiwi.model.ChannelInfo({channel: this.model.channel});
+    },
+
+
+    show: function () {
+        $('#kiwi .memberlists').children().removeClass('active');
+        $(this.el).addClass('active');
+
+        this.renderMeta();
+    }
+});
+
+
+_kiwi.view.MenuBox = Backbone.View.extend({
+    events: {
+        'click .ui_menu_foot .close, a.close_menu': 'dispose'
+    },
+
+    initialize: function(title) {
+        var that = this;
+
+        this.$el = $('<div class="ui_menu"><div class="items"></div></div>');
+
+        this._title = title || '';
+        this._items = {};
+        this._display_footer = true;
+        this._close_on_blur = true;
+    },
+
+
+    render: function() {
+        var that = this,
+            $title,
+            $items = that.$el.find('.items');
+
+        $items.find('*').remove();
+
+        if (this._title) {
+            $title = $('<div class="ui_menu_title"></div>')
+                .text(this._title);
+
+            this.$el.prepend($title);
+        }
+
+        _.each(this._items, function(item) {
+            var $item = $('<div class="ui_menu_content hover"></div>')
+                .append(item);
+
+            $items.append($item);
+        });
+
+        if (this._display_footer)
+            this.$el.append('<div class="ui_menu_foot"><a class="close" onclick="">Close <i class="fa fa-times"></i></a></div>');
+
+    },
+
+
+    setTitle: function(new_title) {
+        this._title = new_title;
+
+        if (!this._title)
+            return;
+
+        this.$el.find('.ui_menu_title').text(this._title);
+    },
+
+
+    onDocumentClick: function(event) {
+        var $target = $(event.target);
+
+        if (!this._close_on_blur)
+            return;
+
+        // If this is not itself AND we don't contain this element, dispose $el
+        if ($target[0] != this.$el[0] && this.$el.has($target).length === 0)
+            this.dispose();
+    },
+
+
+    dispose: function() {
+        _.each(this._items, function(item) {
+            item.dispose && item.dispose();
+            item.remove && item.remove();
+        });
+
+        this._items = null;
+        this.remove();
+
+        if (this._close_proxy)
+            $(document).off('click', this._close_proxy);
+    },
+
+
+    addItem: function(item_name, $item) {
+        if ($item.is('a')) $item.addClass('fa fa-chevron-right');
+        this._items[item_name] = $item;
+    },
+
+
+    removeItem: function(item_name) {
+        delete this._items[item_name];
+    },
+
+
+    showFooter: function(show) {
+        this._display_footer = show;
+    },
+
+
+    closeOnBlur: function(close_it) {
+        this._close_on_blur = close_it;
+    },
+
+
+    show: function() {
+        var that = this,
+            $controlbox, menu_height;
+
+        this.render();
+        this.$el.appendTo(_kiwi.app.view.$el);
+
+        // Ensure the menu doesn't get too tall to overlap the input bar at the bottom
+        $controlbox = _kiwi.app.view.$el.find('.controlbox');
+        $items = this.$el.find('.items');
+        menu_height = this.$el.outerHeight() - $items.outerHeight();
+
+        $items.css({
+            'overflow-y': 'auto',
+            'max-height': $controlbox.offset().top - this.$el.offset().top - menu_height
+        });
+
+        // We add this document click listener on the next javascript tick.
+        // If the current tick is handling an existing click event (such as the nicklist click handler),
+        // the click event bubbles up and hits the document therefore calling this callback to
+        // remove this menubox before it's even shown.
+        setTimeout(function() {
+            that._close_proxy = function(event) {
+                that.onDocumentClick(event);
+            };
+            $(document).on('click', that._close_proxy);
+        }, 0);
+    }
+});
+
+
+
+// Model for this = _kiwi.model.NetworkPanelList
+_kiwi.view.NetworkTabs = Backbone.View.extend({
+    tagName: 'ul',
+    className: 'connections',
+
+    initialize: function() {
+        this.model.on('add', this.networkAdded, this);
+        this.model.on('remove', this.networkRemoved, this);
+
+        this.$el.appendTo(_kiwi.app.view.$el.find('.tabs'));
+    },
+
+    networkAdded: function(network) {
+        $('<li class="connection"></li>')
+            .append(network.panels.view.$el)
+            .appendTo(this.$el);
+    },
+
+    networkRemoved: function(network) {
+        // Remove the containing list element
+        network.panels.view.$el.parent().remove();
+
+        network.panels.view.remove();
+
+        _kiwi.app.view.doLayout();
+    }
+});
+
+
+_kiwi.view.NickChangeBox = Backbone.View.extend({
+    events: {
+        'submit': 'changeNick',
+        'click .cancel': 'close'
+    },
+
+    initialize: function () {
+        var text = {
+            new_nick: _kiwi.global.i18n.translate('client_views_nickchangebox_new').fetch(),
+            change: _kiwi.global.i18n.translate('client_views_nickchangebox_change').fetch(),
+            cancel: _kiwi.global.i18n.translate('client_views_nickchangebox_cancel').fetch()
+        };
+        this.$el = $(_.template($('#tmpl_nickchange').html().trim(), text));
+    },
+
+    render: function () {
+        // Add the UI component and give it focus
+        _kiwi.app.controlbox.$el.prepend(this.$el);
+        this.$el.find('input').focus();
+
+        this.$el.css('bottom', _kiwi.app.controlbox.$el.outerHeight(true));
+    },
+
+    close: function () {
+        this.$el.remove();
+        this.trigger('close');
+    },
+
+    changeNick: function (event) {
+        event.preventDefault();
+
+        var connection = _kiwi.app.connections.active_connection;
+        this.listenTo(connection, 'change:nick', function() {
+            this.close();
+        });
+
+        connection.gateway.changeNick(this.$('input').val());
+    }
+});
+
+
+_kiwi.view.ResizeHandler = Backbone.View.extend({
+    events: {
+        'mousedown': 'startDrag',
+        'mouseup': 'stopDrag'
+    },
+
+    initialize: function () {
+        this.dragging = false;
+        this.starting_width = {};
+
+        $(window).on('mousemove', $.proxy(this.onDrag, this));
+    },
+
+    startDrag: function (event) {
+        this.dragging = true;
+    },
+
+    stopDrag: function (event) {
+        this.dragging = false;
+    },
+
+    onDrag: function (event) {
+        if (!this.dragging) return;
+
+        var offset = $('#kiwi').offset().left;
+
+        this.$el.css('left', event.clientX - (this.$el.outerWidth(true) / 2) - offset);
+        $('#kiwi .right_bar').css('width', this.$el.parent().width() - (this.$el.position().left + this.$el.outerWidth()));
+        _kiwi.app.view.doLayout();
+    }
+});
+
+
+_kiwi.view.ServerSelect = Backbone.View.extend({
+    events: {
+        'submit form': 'submitForm',
+        'click .show_more': 'showMore',
+        'change .have_pass input': 'showPass',
+        'change .have_key input': 'showKey',
+        'click .fa-key': 'channelKeyIconClick',
+        'click .show_server': 'showServer'
+    },
+
+    initialize: function () {
+        var that = this,
+            text = {
+                think_nick: _kiwi.global.i18n.translate('client_views_serverselect_form_title').fetch(),
+                nickname: _kiwi.global.i18n.translate('client_views_serverselect_nickname').fetch(),
+                have_password: _kiwi.global.i18n.translate('client_views_serverselect_enable_password').fetch(),
+                password: _kiwi.global.i18n.translate('client_views_serverselect_password').fetch(),
+                channel: _kiwi.global.i18n.translate('client_views_serverselect_channel').fetch(),
+                channel_key: _kiwi.global.i18n.translate('client_views_serverselect_channelkey').fetch(),
+                require_key: _kiwi.global.i18n.translate('client_views_serverselect_channelkey_required').fetch(),
+                key: _kiwi.global.i18n.translate('client_views_serverselect_key').fetch(),
+                start: _kiwi.global.i18n.translate('client_views_serverselect_connection_start').fetch(),
+                server_network: _kiwi.global.i18n.translate('client_views_serverselect_server_and_network').fetch(),
+                server: _kiwi.global.i18n.translate('client_views_serverselect_server').fetch(),
+                port: _kiwi.global.i18n.translate('client_views_serverselect_port').fetch(),
+                powered_by: _kiwi.global.i18n.translate('client_views_serverselect_poweredby').fetch()
+            };
+
+        this.$el = $(_.template($('#tmpl_server_select').html().trim(), text));
+
+        // Remove the 'more' link if the server has disabled server changing
+        if (_kiwi.app.server_settings && _kiwi.app.server_settings.connection) {
+            if (!_kiwi.app.server_settings.connection.allow_change) {
+                this.$el.find('.show_more').remove();
+                this.$el.addClass('single_server');
+            }
+        }
+
+        // Are currently showing all the controlls or just a nick_change box?
+        this.state = 'all';
+
+        this.more_shown = false;
+
+        this.model.bind('new_network', this.newNetwork, this);
+
+        this.gateway = _kiwi.global.components.Network();
+        this.gateway.on('connect', this.networkConnected, this);
+        this.gateway.on('connecting', this.networkConnecting, this);
+        this.gateway.on('disconnect', this.networkDisconnected, this);
+        this.gateway.on('irc_error', this.onIrcError, this);
+    },
+
+    dispose: function() {
+        this.model.off('new_network', this.newNetwork, this);
+        this.gateway.off();
+
+        this.remove();
+    },
+
+    submitForm: function (event) {
+        event.preventDefault();
+
+        // Make sure a nick is chosen
+        if (!$('input.nick', this.$el).val().trim()) {
+            this.setStatus(_kiwi.global.i18n.translate('client_views_serverselect_nickname_error_empty').fetch());
+            $('input.nick', this.$el).select();
+            return;
+        }
+
+        if (this.state === 'nick_change') {
+            this.submitNickChange(event);
+        } else {
+            this.submitLogin(event);
+        }
+
+        $('button', this.$el).attr('disabled', 1);
+        return;
+    },
+
+    submitLogin: function (event) {
+        // If submitting is disabled, don't do anything
+        if ($('button', this.$el).attr('disabled')) return;
+
+        var values = {
+            nick: $('input.nick', this.$el).val(),
+            server: $('input.server', this.$el).val(),
+            port: $('input.port', this.$el).val(),
+            ssl: $('input.ssl', this.$el).prop('checked'),
+            password: $('input.password', this.$el).val(),
+            channel: $('input.channel', this.$el).val(),
+            channel_key: $('input.channel_key', this.$el).val(),
+            options: this.server_options
+        };
+
+        this.trigger('server_connect', values);
+    },
+
+    submitNickChange: function (event) {
+        _kiwi.gateway.changeNick(null, $('input.nick', this.$el).val());
+        this.networkConnecting();
+    },
+
+    showPass: function (event) {
+        if (this.$el.find('tr.have_pass input').is(':checked')) {
+            this.$el.find('tr.pass').show().find('input').focus();
+        } else {
+            this.$el.find('tr.pass').hide().find('input').val('');
+        }
+    },
+
+    channelKeyIconClick: function (event) {
+        this.$el.find('tr.have_key input').click();
+    },
+
+    showKey: function (event) {
+        if (this.$el.find('tr.have_key input').is(':checked')) {
+            this.$el.find('tr.key').show().find('input').focus();
+        } else {
+            this.$el.find('tr.key').hide().find('input').val('');
+        }
+    },
+
+    showMore: function (event) {
+        if (!this.more_shown) {
+            $('.more', this.$el).slideDown('fast');
+            $('.show_more', this.$el)
+                .children('.fa-caret-down')
+                .removeClass('fa-caret-down')
+                .addClass('fa-caret-up');
+            $('input.server', this.$el).select();
+            this.more_shown = true;
+        } else {
+            $('.more', this.$el).slideUp('fast');
+            $('.show_more', this.$el)
+                .children('.fs-caret-up')
+                .removeClass('fa-caret-up')
+                .addClass('fa-caret-down');
+            $('input.nick', this.$el).select();
+            this.more_shown = false;
+        }
+    },
+
+    populateFields: function (defaults) {
+        var nick, server, port, channel, channel_key, ssl, password;
+
+        defaults = defaults || {};
+
+        nick = defaults.nick || '';
+        server = defaults.server || '';
+        port = defaults.port || 6667;
+        ssl = defaults.ssl || 0;
+        password = defaults.password || '';
+        channel = defaults.channel || '';
+        channel_key = defaults.channel_key || '';
+
+        $('input.nick', this.$el).val(nick);
+        $('input.server', this.$el).val(server);
+        $('input.port', this.$el).val(port);
+        $('input.ssl', this.$el).prop('checked', ssl);
+        $('input#server_select_show_pass', this.$el).prop('checked', !(!password));
+        $('input.password', this.$el).val(password);
+        if (!(!password)) {
+            $('tr.pass', this.$el).show();
+        }
+        $('input.channel', this.$el).val(channel);
+        $('input#server_select_show_channel_key', this.$el).prop('checked', !(!channel_key));
+        $('input.channel_key', this.$el).val(channel_key);
+        if (!(!channel_key)) {
+            $('tr.key', this.$el).show();
+        }
+
+        // Temporary values
+        this.server_options = {};
+
+        if (defaults.encoding)
+            this.server_options.encoding = defaults.encoding;
+    },
+
+    hide: function () {
+        this.$el.slideUp();
+    },
+
+    show: function (new_state) {
+        new_state = new_state || 'all';
+
+        this.$el.show();
+
+        if (new_state === 'all') {
+            $('.show_more', this.$el).show();
+
+        } else if (new_state === 'more') {
+            $('.more', this.$el).slideDown('fast');
+
+        } else if (new_state === 'nick_change') {
+            $('.more', this.$el).hide();
+            $('.show_more', this.$el).hide();
+            $('input.nick', this.$el).select();
+
+        } else if (new_state === 'enter_password') {
+            $('.more', this.$el).hide();
+            $('.show_more', this.$el).hide();
+            $('input.password', this.$el).select();
+        }
+
+        this.state = new_state;
+    },
+
+    infoBoxShow: function() {
+        var $side_panel = this.$el.find('.side_panel');
+
+        // Some theme may hide the info panel so check before we
+        // resize ourselves
+        if (!$side_panel.is(':visible'))
+            return;
+
+        this.$el.animate({
+            width: parseInt($side_panel.css('left'), 10) + $side_panel.find('.content:first').outerWidth()
+        });
+    },
+
+    infoBoxHide: function() {
+        var $side_panel = this.$el.find('.side_panel');
+        this.$el.animate({
+            width: parseInt($side_panel.css('left'), 10)
+        });
+    },
+
+    infoBoxSet: function($info_view) {
+        this.$el.find('.side_panel .content')
+            .empty()
+            .append($info_view);
+    },
+
+    setStatus: function (text, class_name) {
+        $('.status', this.$el)
+            .text(text)
+            .attr('class', 'status')
+            .addClass(class_name||'')
+            .show();
+    },
+    clearStatus: function () {
+        $('.status', this.$el).hide();
+    },
+
+    reset: function() {
+        this.populateFields();
+        this.clearStatus();
+
+        this.$('button').attr('disabled', null);
+    },
+
+    newNetwork: function(network) {
+        // Keep a reference to this network so we can interact with it
+        this.model.current_connecting_network = network;
+    },
+
+    networkConnected: function (event) {
+        this.model.trigger('connected', _kiwi.app.connections.getByConnectionId(event.server));
+        this.model.current_connecting_network = null;
+    },
+
+    networkDisconnected: function () {
+        this.model.current_connecting_network = null;
+        this.state = 'all';
+    },
+
+    networkConnecting: function (event) {
+        this.model.trigger('connecting');
+        this.setStatus(_kiwi.global.i18n.translate('client_views_serverselect_connection_trying').fetch(), 'ok');
+
+        this.$('.status').append('<a class="show_server"><i class="fa fa-info-circle"></i></a>');
+    },
+
+    showServer: function() {
+        // If we don't have a current connection in the making then we have nothing to show
+        if (!this.model.current_connecting_network)
+            return;
+
+        _kiwi.app.view.barsShow();
+        this.model.current_connecting_network.panels.server.view.show();
+    },
+
+    onIrcError: function (data) {
+        $('button', this.$el).attr('disabled', null);
+
+        switch(data.error) {
+        case 'nickname_in_use':
+            this.setStatus(_kiwi.global.i18n.translate('client_views_serverselect_nickname_error_alreadyinuse').fetch());
+            this.show('nick_change');
+            this.$el.find('.nick').select();
+            break;
+        case 'erroneus_nickname':
+            if (data.reason) {
+                this.setStatus(data.reason);
+            } else {
+                this.setStatus(_kiwi.global.i18n.translate('client_views_serverselect_nickname_invalid').fetch());
+            }
+            this.show('nick_change');
+            this.$el.find('.nick').select();
+            break;
+        case 'password_mismatch':
+            this.setStatus(_kiwi.global.i18n.translate('client_views_serverselect_password_incorrect').fetch());
+            this.show('enter_password');
+            this.$el.find('.password').select();
+            break;
+        default:
+            this.showError(data.reason || '');
+            break;
+        }
+    },
+
+    showError: function (error_reason) {
+        var err_text = _kiwi.global.i18n.translate('client_views_serverselect_connection_error').fetch();
+
+        if (error_reason) {
+            switch (error_reason) {
+            case 'ENOTFOUND':
+                err_text = _kiwi.global.i18n.translate('client_views_serverselect_server_notfound').fetch();
+                break;
+
+            case 'ECONNREFUSED':
+                err_text += ' (' + _kiwi.global.i18n.translate('client_views_serverselect_connection_refused').fetch() + ')';
+                break;
+
+            default:
+                err_text += ' (' + error_reason + ')';
+            }
+        }
+
+        this.setStatus(err_text, 'error');
+        $('button', this.$el).attr('disabled', null);
+        this.show();
+    }
+});
+
+
+_kiwi.view.StatusMessage = Backbone.View.extend({
+    initialize: function () {
+        this.$el.hide();
+
+        // Timer for hiding the message after X seconds
+        this.tmr = null;
+    },
+
+    text: function (text, opt) {
+        // Defaults
+        opt = opt || {};
+        opt.type = opt.type || '';
+        opt.timeout = opt.timeout || 5000;
+
+        this.$el.text(text).addClass(opt.type);
+        this.$el.slideDown($.proxy(_kiwi.app.view.doLayout, _kiwi.app.view));
+
+        if (opt.timeout) this.doTimeout(opt.timeout);
+    },
+
+    html: function (html, opt) {
+        // Defaults
+        opt = opt || {};
+        opt.type = opt.type || '';
+        opt.timeout = opt.timeout || 5000;
+
+        this.$el.html(html).addClass(opt.type);
+        this.$el.slideDown($.proxy(_kiwi.app.view.doLayout, _kiwi.app.view));
+
+        if (opt.timeout) this.doTimeout(opt.timeout);
+    },
+
+    hide: function () {
+        this.$el.slideUp($.proxy(_kiwi.app.view.doLayout, _kiwi.app.view));
+    },
+
+    doTimeout: function (length) {
+        if (this.tmr) clearTimeout(this.tmr);
+        var that = this;
+        this.tmr = setTimeout(function () { that.hide(); }, length);
+    }
+});
+
+
+// Model for this = _kiwi.model.PanelList
+_kiwi.view.Tabs = Backbone.View.extend({
+    tagName: 'ul',
+    className: 'panellist',
+
+    events: {
+        'click li': 'tabClick',
+        'click li .part': 'partClick'
+    },
+
+    initialize: function () {
+        this.model.on("add", this.panelAdded, this);
+        this.model.on("remove", this.panelRemoved, this);
+        this.model.on("reset", this.render, this);
+
+        this.model.on('active', this.panelActive, this);
+
+        // Network tabs start with a server, so determine what we are now
+        this.is_network = false;
+
+        if (this.model.network) {
+            this.is_network = true;
+
+            this.model.network.on('change:name', function (network, new_val) {
+                $('span', this.model.server.tab).text(new_val);
+            }, this);
+
+            this.model.network.on('change:connection_id', function (network, new_val) {
+                this.model.forEach(function(panel) {
+                    panel.tab.data('connection_id', new_val);
+                });
+            }, this);
+        }
+    },
+
+    render: function () {
+        var that = this;
+
+        this.$el.empty();
+
+        if (this.is_network) {
+            // Add the server tab first
+            this.model.server.tab
+                .data('panel', this.model.server)
+                .data('connection_id', this.model.network.get('connection_id'))
+                .appendTo(this.$el);
+        }
+
+        // Go through each panel adding its tab
+        this.model.forEach(function (panel) {
+            // If this is the server panel, ignore as it's already added
+            if (this.is_network && panel == that.model.server)
+                return;
+
+            panel.tab.data('panel', panel);
+
+            if (this.is_network)
+                panel.tab.data('connection_id', this.model.network.get('connection_id'));
+
+            panel.tab.appendTo(that.$el);
+        });
+
+        _kiwi.app.view.doLayout();
+    },
+
+    updateTabTitle: function (panel, new_title) {
+        $('span', panel.tab).text(new_title);
+    },
+
+    panelAdded: function (panel) {
+        // Add a tab to the panel
+        panel.tab = $('<li><span></span><div class="activity"></div></li>');
+        panel.tab.find('span').text(panel.get('title') || panel.get('name'));
+
+        if (panel.isServer()) {
+            panel.tab.addClass('server');
+            panel.tab.addClass('fa');
+            panel.tab.addClass('fa-nonexistant');
+        }
+
+        panel.tab.data('panel', panel);
+
+        if (this.is_network)
+            panel.tab.data('connection_id', this.model.network.get('connection_id'));
+
+        this.sortTabs();
+
+        panel.bind('change:title', this.updateTabTitle);
+        panel.bind('change:name', this.updateTabTitle);
+
+        _kiwi.app.view.doLayout();
+    },
+    panelRemoved: function (panel) {
+        var connection = _kiwi.app.connections.active_connection;
+
+        panel.tab.remove();
+        delete panel.tab;
+
+        _kiwi.app.panels.trigger('remove', panel);
+
+        _kiwi.app.view.doLayout();
+    },
+
+    panelActive: function (panel, previously_active_panel) {
+        // Remove any existing tabs or part images
+        _kiwi.app.view.$el.find('.panellist .part').remove();
+        _kiwi.app.view.$el.find('.panellist .active').removeClass('active');
+
+        panel.tab.addClass('active');
+
+        panel.tab.append('<span class="part fa fa-nonexistant"></span>');
+    },
+
+    tabClick: function (e) {
+        var tab = $(e.currentTarget);
+
+        var panel = tab.data('panel');
+        if (!panel) {
+            // A panel wasn't found for this tab... wadda fuck
+            return;
+        }
+
+        panel.view.show();
+    },
+
+    partClick: function (e) {
+        var tab = $(e.currentTarget).parent();
+        var panel = tab.data('panel');
+
+        if (!panel) return;
+
+        // If the nicklist is empty, we haven't joined the channel as yet
+        // If we part a server, then we need to disconnect from server, close channel tabs,
+        // close server tab, then bring client back to homepage
+        if (panel.isChannel() && panel.get('members').models.length > 0) {
+            this.model.network.gateway.part(panel.get('name'));
+
+        } else if(panel.isServer()) {
+            if (!this.model.network.get('connected') || confirm(translateText('disconnect_from_server'))) {
+                this.model.network.gateway.quit("Leaving");
+                _kiwi.app.connections.remove(this.model.network);
+                _kiwi.app.startup_applet.view.show();
+            }
+
+        } else {
+            panel.close();
+        }
+    },
+
+    sortTabs: function() {
+        var that = this,
+            panels = [];
+
+        this.model.forEach(function (panel) {
+            // Ignore the server tab, so all others get added after it
+            if (that.is_network && panel == that.model.server)
+                return;
+
+            panels.push([panel.get('title') || panel.get('name'), panel]);
+        });
+
+        // Sort by the panel name..
+        panels.sort(function(a, b) {
+            if (a[0].toLowerCase() > b[0].toLowerCase()) {
+                return 1;
+            } else if (a[0].toLowerCase() < b[0].toLowerCase()) {
+                return -1;
+            } else {
+                return 0;
+            }
+        });
+
+        // And add them all back in order.
+        _.each(panels, function(panel) {
+            panel[1].tab.appendTo(that.$el);
+        });
+    }
+});
+
+
+_kiwi.view.TopicBar = Backbone.View.extend({
+    events: {
+        'keydown div': 'process'
+    },
+
+    initialize: function () {
+        _kiwi.app.panels.bind('active', function (active_panel) {
+            // If it's a channel topic, update and make editable
+            if (active_panel.isChannel()) {
+                this.setCurrentTopicFromChannel(active_panel);
+                this.$el.find('div').attr('contentEditable', true);
+
+            } else {
+                // Not a channel topic.. clear and make uneditable
+                this.$el.find('div').attr('contentEditable', false)
+                    .text('');
+            }
+        }, this);
+    },
+
+    process: function (ev) {
+        var inp = $(ev.currentTarget),
+            inp_val = inp.text();
+
+        // Only allow topic editing if this is a channel panel
+        if (!_kiwi.app.panels().active.isChannel()) {
+            return false;
+        }
+
+        // If hit return key, update the current topic
+        if (ev.keyCode === 13) {
+            _kiwi.app.connections.active_connection.gateway.topic(_kiwi.app.panels().active.get('name'), inp_val);
+            return false;
+        }
+    },
+
+    setCurrentTopic: function (new_topic) {
+        new_topic = new_topic || '';
+
+        // We only want a plain text version
+        $('div', this.$el).html(formatIRCMsg(_.escape(new_topic)));
+    },
+
+    setCurrentTopicFromChannel: function(channel) {
+        var set_by = channel.get('topic_set_by'),
+            set_by_text = '';
+
+        this.setCurrentTopic(channel.get("topic"));
+
+        if (set_by) {
+            set_by_text += translateText('client_models_network_topic', [set_by.nick, _kiwi.utils.formatDate(set_by.when)]);
+            this.$el.attr('title', set_by_text);
+        } else {
+            this.$el.attr('title', '');
+        }
+    }
+});
+
+
+_kiwi.view.UserBox = Backbone.View.extend({
+    events: {
+        'click .query': 'queryClick',
+        'click .info': 'infoClick',
+        'change .ignore': 'ignoreChange',
+        'click .ignore': 'ignoreClick',
+        'click .op': 'opClick',
+        'click .deop': 'deopClick',
+        'click .voice': 'voiceClick',
+        'click .devoice': 'devoiceClick',
+        'click .kick': 'kickClick',
+        'click .ban': 'banClick'
+    },
+
+    initialize: function () {
+        var text = {
+            op: _kiwi.global.i18n.translate('client_views_userbox_op').fetch(),
+            de_op: _kiwi.global.i18n.translate('client_views_userbox_deop').fetch(),
+            voice: _kiwi.global.i18n.translate('client_views_userbox_voice').fetch(),
+            de_voice: _kiwi.global.i18n.translate('client_views_userbox_devoice').fetch(),
+            kick: _kiwi.global.i18n.translate('client_views_userbox_kick').fetch(),
+            ban: _kiwi.global.i18n.translate('client_views_userbox_ban').fetch(),
+            message: _kiwi.global.i18n.translate('client_views_userbox_query').fetch(),
+            info: _kiwi.global.i18n.translate('client_views_userbox_whois').fetch(),
+            ignore: _kiwi.global.i18n.translate('client_views_userbox_ignore').fetch()
+        };
+        this.$el = $(_.template($('#tmpl_userbox').html().trim(), text));
+    },
+
+    setTargets: function (user, channel) {
+        this.user = user;
+        this.channel = channel;
+
+        var is_ignored = _kiwi.app.connections.active_connection.isNickIgnored(this.user.get('nick'));
+        this.$('.ignore input').attr('checked', is_ignored ? 'checked' : false);
+    },
+
+    displayOpItems: function(display_items) {
+        if (display_items) {
+            this.$el.find('.if_op').css('display', 'block');
+        } else {
+            this.$el.find('.if_op').css('display', 'none');
+        }
+    },
+
+    queryClick: function (event) {
+        var nick = this.user.get('nick');
+        _kiwi.app.connections.active_connection.createQuery(nick);
+    },
+
+    infoClick: function (event) {
+        _kiwi.app.controlbox.processInput('/whois ' + this.user.get('nick'));
+    },
+
+    ignoreClick: function (event) {
+        // Stop the menubox from closing since it will not update the checkbox otherwise
+        event.stopPropagation();
+    },
+
+    ignoreChange: function (event) {
+        if ($(event.currentTarget).find('input').is(':checked')) {
+            _kiwi.app.controlbox.processInput('/ignore ' + this.user.get('nick'));
+        } else {
+            _kiwi.app.controlbox.processInput('/unignore ' + this.user.get('nick'));
+        }
+    },
+
+    opClick: function (event) {
+        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +o ' + this.user.get('nick'));
+    },
+
+    deopClick: function (event) {
+        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' -o ' + this.user.get('nick'));
+    },
+
+    voiceClick: function (event) {
+        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +v ' + this.user.get('nick'));
+    },
+
+    devoiceClick: function (event) {
+        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' -v ' + this.user.get('nick'));
+    },
+
+    kickClick: function (event) {
+        // TODO: Enable the use of a custom kick message
+        _kiwi.app.controlbox.processInput('/kick ' + this.user.get('nick') + ' Bye!');
+    },
+
+    banClick: function (event) {
+        // TODO: Set ban on host, not just on nick
+        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +b ' + this.user.get('nick') + '!*');
+    }
+});
+
+
+_kiwi.view.ChannelTools = Backbone.View.extend({
+    events: {
+        'click .channel_info': 'infoClick',
+        'click .channel_part': 'partClick'
+    },
+
+    initialize: function () {},
+
+    infoClick: function (event) {
+        new _kiwi.model.ChannelInfo({channel: _kiwi.app.panels().active});
+    },
+
+    partClick: function (event) {
+        _kiwi.app.connections.active_connection.gateway.part(_kiwi.app.panels().active.get('name'));
+    }
+});
+
+
+// var f = new _kiwi.model.ChannelInfo({channel: _kiwi.app.panels().active});
+
+_kiwi.view.ChannelInfo = Backbone.View.extend({
+    events: {
+        'click .toggle_banlist': 'toggleBanList',
+        'change .channel-mode': 'onModeChange',
+        'click .remove-ban': 'onRemoveBanClick'
+    },
+
+
+    initialize: function () {
+        var that = this,
+            network,
+            channel = this.model.get('channel'),
+            text;
+
+        text = {
+            moderated_chat: translateText('client_views_channelinfo_moderated'),
+            invite_only: translateText('client_views_channelinfo_inviteonly'),
+            ops_change_topic: translateText('client_views_channelinfo_opschangechannel'),
+            external_messages: translateText('client_views_channelinfo_externalmessages'),
+            toggle_banlist: translateText('client_views_channelinfo_togglebanlist'),
+            channel_name: channel.get('name')
+        };
+
+        this.$el = $(_.template($('#tmpl_channel_info').html().trim(), text));
+
+        // Create the menu box this view will sit inside
+        this.menu = new _kiwi.view.MenuBox(channel.get('name'));
+        this.menu.addItem('channel_info', this.$el);
+        this.menu.$el.appendTo(channel.view.$container);
+        this.menu.show();
+
+        this.menu.$el.offset({top: _kiwi.app.view.$el.find('.panels').offset().top});
+
+        // Menu box will call this destroy on closing
+        this.$el.dispose = _.bind(this.dispose, this);
+
+        // Display the info we have, then listen for further changes
+        this.updateInfo(channel);
+        channel.on('change:info_modes change:info_url change:banlist', this.updateInfo, this);
+
+        // Request the latest info for ths channel from the network
+        channel.get('network').gateway.channelInfo(channel.get('name'));
+    },
+
+
+    render: function () {
+    },
+
+
+    onModeChange: function(event) {
+        var $this = $(event.currentTarget),
+            channel = this.model.get('channel'),
+            mode = $this.data('mode'),
+            mode_string = '';
+
+        if ($this.attr('type') == 'checkbox') {
+            mode_string = $this.is(':checked') ? '+' : '-';
+            mode_string += mode;
+            channel.setMode(mode_string);
+
+            return;
+        }
+
+        if ($this.attr('type') == 'text') {
+            mode_string = $this.val() ?
+                '+' + mode + ' ' + $this.val() :
+                '-' + mode;
+
+            channel.setMode(mode_string);
+
+            return;
+        }
+    },
+
+
+    onRemoveBanClick: function (event) {
+        event.preventDefault();
+        event.stopPropagation();
+
+        var $this = $(event.currentTarget),
+            $tr = $this.parents('tr:first'),
+            ban = $tr.data('ban');
+
+        if (!ban)
+            return;
+
+        var channel = this.model.get('channel');
+        channel.setMode('-b ' + ban.banned);
+
+        $tr.remove();
+    },
+
+
+    updateInfo: function (channel, new_val) {
+        var that = this,
+            title, modes, url, banlist;
+
+        modes = channel.get('info_modes');
+        if (modes) {
+            _.each(modes, function(mode, idx) {
+                mode.mode = mode.mode.toLowerCase();
+
+                if (mode.mode == '+k') {
+                    that.$el.find('[name="channel_key"]').val(mode.param);
+                } else if (mode.mode == '+m') {
+                    that.$el.find('[name="channel_mute"]').attr('checked', 'checked');
+                } else if (mode.mode == '+i') {
+                    that.$el.find('[name="channel_invite"]').attr('checked', 'checked');
+                } else if (mode.mode == '+n') {
+                    that.$el.find('[name="channel_external_messages"]').attr('checked', 'checked');
+                } else if (mode.mode == '+t') {
+                    that.$el.find('[name="channel_topic"]').attr('checked', 'checked');
+                }
+            });
+        }
+
+        url = channel.get('info_url');
+        if (url) {
+            this.$el.find('.channel_url')
+                .text(url)
+                .attr('href', url);
+
+            this.$el.find('.channel_url').slideDown();
+        }
+
+        banlist = channel.get('banlist');
+        if (banlist && banlist.length) {
+            var $table = this.$el.find('.channel-banlist table tbody');
+
+            this.$el.find('.banlist-status').text('');
+
+            $table.empty();
+            _.each(banlist, function(ban) {
+                var $tr = $('<tr></tr>').data('ban', ban);
+
+                $('<td></td>').text(ban.banned).appendTo($tr);
+                $('<td></td>').text(ban.banned_by.split(/[!@]/)[0]).appendTo($tr);
+                $('<td></td>').text(_kiwi.utils.formatDate(new Date(parseInt(ban.banned_at, 10) * 1000))).appendTo($tr);
+                $('<td><i class="fa fa-rtimes remove-ban"></i></td>').appendTo($tr);
+
+                $table.append($tr);
+            });
+
+            this.$el.find('.channel-banlist table').slideDown();
+        } else {
+            this.$el.find('.banlist-status').text('Banlist empty');
+            this.$el.find('.channel-banlist table').hide();
+        }
+    },
+
+    toggleBanList: function (event) {
+        event.preventDefault();
+        this.$el.find('.channel-banlist table').toggle();
+
+        if(!this.$el.find('.channel-banlist table').is(':visible'))
+            return;
+
+        var channel = this.model.get('channel'),
+            network = channel.get('network');
+
+        network.gateway.raw('MODE ' + channel.get('name') + ' +b');
+    },
+
+    dispose: function () {
+        this.model.get('channel').off('change:info_modes change:info_url change:banlist', this.updateInfo, this);
+
+        this.$el.remove();
+    }
+});
+
+
+
+_kiwi.view.RightBar = Backbone.View.extend({
+    events: {
+        'click .right-bar-toggle': 'onClickToggle',
+        'click .right-bar-toggle-inner': 'onClickToggle'
+    },
+
+    initialize: function() {
+        this.keep_hidden = false;
+        this.hidden = this.$el.hasClass('disabled');
+
+        this.updateIcon();
+    },
+
+
+    hide: function() {
+        this.hidden = true;
+        this.$el.addClass('disabled');
+
+        this.updateIcon();
+    },
+
+
+    show: function() {
+        this.hidden = false;
+
+        if (!this.keep_hidden)
+            this.$el.removeClass('disabled');
+
+        this.updateIcon();
+    },
+
+
+    // Toggle if the rightbar should be shown or not
+    toggle: function(keep_hidden) {
+        // Hacky, but we need to ignore the toggle() call from doLayout() as we are overriding it
+        if (this.ignore_layout)
+            return true;
+
+        if (typeof keep_hidden === 'undefined') {
+            this.keep_hidden = !this.keep_hidden;
+        } else {
+            this.keep_hidden = keep_hidden;
+        }
+
+        if (this.keep_hidden || this.hidden) {
+            this.$el.addClass('disabled');
+        } else {
+            this.$el.removeClass('disabled');
+        }
+
+        this.updateIcon();
+    },
+
+
+    updateIcon: function() {
+        var $toggle = this.$('.right-bar-toggle'),
+            $icon = $toggle.find('i');
+
+        if (!this.hidden && this.keep_hidden) {
+            $toggle.show();
+        } else {
+            $toggle.hide();
+        }
+
+        if (this.keep_hidden) {
+            $icon.removeClass('fa fa-angle-double-right').addClass('fa fa-users');
+        } else {
+            $icon.removeClass('fa fa-users').addClass('fa fa-angle-double-right');
+        }
+    },
+
+
+    onClickToggle: function(event) {
+        this.toggle();
+
+        // Hacky, but we need to ignore the toggle() call from doLayout() as we are overriding it
+        this.ignore_layout = true;
+        _kiwi.app.view.doLayout();
+
+        // No longer ignoring the toggle() call from doLayout()
+        delete this.ignore_layout;
+    }
+});
+
+
+_kiwi.view.Notification = Backbone.View.extend({
+    className: 'notification',
+
+    events: {
+        'click .close': 'close'
+    },
+
+    initialize: function(title, content) {
+        this.title = title;
+        this.content = content;
+    },
+
+    render: function() {
+        this.$el.html($('#tmpl_notifications').html());
+        this.$('h6').text(this.title);
+
+        // HTML string or jquery object
+        if (typeof this.content === 'string') {
+                this.$('.content').html(this.content);
+            } else if (typeof this.content === 'object') {
+                this.$('.content').empty().append(this.content);
+            }
+
+        return this;
+    },
+
+    show: function() {
+        var that = this;
+
+        this.render().$el.appendTo(_kiwi.app.view.$el);
+
+        // The element won't have any CSS transitions applied
+        // until after a tick + paint.
+        _.defer(function() {
+            that.$el.addClass('show');
+        });
+    },
+
+    close: function() {
+        this.remove();
+    }
+});
+
+
+(function() {
+
+    function ClientUiCommands(app, controlbox) {
+        this.app = app;
+        this.controlbox = controlbox;
+
+        this.addDefaultAliases();
+        this.bindCommand(fn_to_bind);
+    }
+
+    _kiwi.misc.ClientUiCommands = ClientUiCommands;
+
+
+    // Add the default user command aliases
+    ClientUiCommands.prototype.addDefaultAliases = function() {
+        $.extend(this.controlbox.preprocessor.aliases, {
+            // General aliases
+            '/p':        '/part $1+',
+            '/me':       '/action $1+',
+            '/j':        '/join $1+',
+            '/q':        '/query $1+',
+            '/w':        '/whois $1+',
+            '/raw':      '/quote $1+',
+            '/connect':  '/server $1+',
+
+            // Op related aliases
+            '/op':       '/quote mode $channel +o $1+',
+            '/deop':     '/quote mode $channel -o $1+',
+            '/hop':      '/quote mode $channel +h $1+',
+            '/dehop':    '/quote mode $channel -h $1+',
+            '/voice':    '/quote mode $channel +v $1+',
+            '/devoice':  '/quote mode $channel -v $1+',
+            '/k':        '/kick $channel $1+',
+            '/ban':      '/quote mode $channel +b $1+',
+            '/unban':    '/quote mode $channel -b $1+',
+
+            // Misc aliases
+            '/slap':     '/me slaps $1 around a bit with a large trout',
+            '/tick':     '/msg $channel âœ”'
+        });
+    };
+
+
+    /**
+     * Add a new command action
+     * @var command Object {'command:the_command': fn}
+     */
+    ClientUiCommands.prototype.bindCommand = function(command) {
+        var that = this;
+
+        _.each(command, function(fn, event_name) {
+            that.controlbox.on(event_name, _.bind(fn, that));
+        });
+    };
+
+
+
+
+    /**
+     * Default functions to bind to controlbox events
+     **/
+
+    var fn_to_bind = {
+        'unknown_command':     unknownCommand,
+        'command':             allCommands,
+        'command:msg':         msgCommand,
+        'command:action':      actionCommand,
+        'command:join':        joinCommand,
+        'command:part':        partCommand,
+        'command:cycle':        cycleCommand,
+        'command:nick':        nickCommand,
+        'command:query':       queryCommand,
+        'command:invite':      inviteCommand,
+        'command:topic':       topicCommand,
+        'command:notice':      noticeCommand,
+        'command:quote':       quoteCommand,
+        'command:kick':        kickCommand,
+        'command:clear':       clearCommand,
+        'command:ctcp':        ctcpCommand,
+        'command:quit':        quitCommand,
+        'command:server':      serverCommand,
+        'command:whois':       whoisCommand,
+        'command:whowas':      whowasCommand,
+        'command:away':        awayCommand,
+        'command:encoding':    encodingCommand,
+        'command:channel':     channelCommand,
+        'command:applet':      appletCommand,
+        'command:settings':    settingsCommand,
+        'command:script':      scriptCommand
+    };
+
+
+    fn_to_bind['command:css'] = function (ev) {
+        var queryString = '?reload=' + new Date().getTime();
+        $('link[rel="stylesheet"]').each(function () {
+            this.href = this.href.replace(/\?.*|$/, queryString);
+        });
+    };
+
+
+    fn_to_bind['command:js'] = function (ev) {
+        if (!ev.params[0]) return;
+        $script(ev.params[0] + '?' + (new Date().getTime()));
+    };
+
+
+    fn_to_bind['command:set'] = function (ev) {
+        if (!ev.params[0]) return;
+
+        var setting = ev.params[0],
+            value;
+
+        // Do we have a second param to set a value?
+        if (ev.params[1]) {
+            ev.params.shift();
+
+            value = ev.params.join(' ');
+
+            // If we're setting a true boolean value..
+            if (value === 'true')
+                value = true;
+
+            // If we're setting a false boolean value..
+            if (value === 'false')
+                value = false;
+
+            // If we're setting a number..
+            if (parseInt(value, 10).toString() === value)
+                value = parseInt(value, 10);
+
+            _kiwi.global.settings.set(setting, value);
+        }
+
+        // Read the value to the user
+        this.app.panels().active.addMsg('', styleText('set_setting', {text: setting + ' = ' + _kiwi.global.settings.get(setting).toString()}));
+    };
+
+
+    fn_to_bind['command:save'] = function (ev) {
+        _kiwi.global.settings.save();
+        this.app.panels().active.addMsg('', styleText('settings_saved', {text: translateText('client_models_application_settings_saved')}));
+    };
+
+
+    fn_to_bind['command:alias'] = function (ev) {
+        var that = this,
+            name, rule;
+
+        // No parameters passed so list them
+        if (!ev.params[1]) {
+            $.each(this.controlbox.preprocessor.aliases, function (name, rule) {
+                that.app.panels().server.addMsg(' ', styleText('list_aliases', {text: name + '   =>   ' + rule}));
+            });
+            return;
+        }
+
+        // Deleting an alias?
+        if (ev.params[0] === 'del' || ev.params[0] === 'delete') {
+            name = ev.params[1];
+            if (name[0] !== '/') name = '/' + name;
+            delete this.controlbox.preprocessor.aliases[name];
+            return;
+        }
+
+        // Add the alias
+        name = ev.params[0];
+        ev.params.shift();
+        rule = ev.params.join(' ');
+
+        // Make sure the name starts with a slash
+        if (name[0] !== '/') name = '/' + name;
+
+        // Now actually add the alias
+        this.controlbox.preprocessor.aliases[name] = rule;
+    };
+
+
+    fn_to_bind['command:ignore'] = function (ev) {
+        var that = this,
+            list = this.app.connections.active_connection.get('ignore_list');
+
+        // No parameters passed so list them
+        if (!ev.params[0]) {
+            if (list.length > 0) {
+                this.app.panels().active.addMsg(' ', styleText('ignore_title', {text: translateText('client_models_application_ignore_title')}));
+                $.each(list, function (idx, ignored_pattern) {
+                    that.app.panels().active.addMsg(' ', styleText('ignored_pattern', {text: ignored_pattern}));
+                });
+            } else {
+                this.app.panels().active.addMsg(' ', styleText('ignore_none', {text: translateText('client_models_application_ignore_none')}));
+            }
+            return;
+        }
+
+        // We have a parameter, so add it
+        list.push(ev.params[0]);
+        this.app.connections.active_connection.set('ignore_list', list);
+        this.app.panels().active.addMsg(' ', styleText('ignore_nick', {text: translateText('client_models_application_ignore_nick', [ev.params[0]])}));
+    };
+
+
+    fn_to_bind['command:unignore'] = function (ev) {
+        var list = this.app.connections.active_connection.get('ignore_list');
+
+        if (!ev.params[0]) {
+            this.app.panels().active.addMsg(' ', styleText('ignore_stop_notice', {text: translateText('client_models_application_ignore_stop_notice')}));
+            return;
+        }
+
+        list = _.reject(list, function(pattern) {
+            return pattern === ev.params[0];
+        });
+
+        this.app.connections.active_connection.set('ignore_list', list);
+
+        this.app.panels().active.addMsg(' ', styleText('ignore_stopped', {text: translateText('client_models_application_ignore_stopped', [ev.params[0]])}));
+    };
+
+
+
+
+    // A fallback action. Send a raw command to the server
+    function unknownCommand (ev) {
+        var raw_cmd = ev.command + ' ' + ev.params.join(' ');
+        this.app.connections.active_connection.gateway.raw(raw_cmd);
+    }
+
+
+    function allCommands (ev) {}
+
+
+    function joinCommand (ev) {
+        var panels, channel_names;
+
+        channel_names = ev.params.join(' ').split(',');
+        panels = this.app.connections.active_connection.createAndJoinChannels(channel_names);
+
+        // Show the last channel if we have one
+        if (panels.length)
+            panels[panels.length - 1].view.show();
+    }
+
+
+    function queryCommand (ev) {
+        var destination, message, panel;
+
+        destination = ev.params[0];
+        ev.params.shift();
+
+        message = ev.params.join(' ');
+
+        // Check if we have the panel already. If not, create it
+        panel = this.app.connections.active_connection.panels.getByName(destination);
+        if (!panel) {
+            panel = new _kiwi.model.Query({name: destination});
+            this.app.connections.active_connection.panels.add(panel);
+        }
+
+        if (panel) panel.view.show();
+
+        if (message) {
+            this.app.connections.active_connection.gateway.msg(panel.get('name'), message);
+            panel.addMsg(this.app.connections.active_connection.get('nick'), styleText('privmsg', {text: message}), 'privmsg');
+        }
+
+    }
+
+
+    function msgCommand (ev) {
+        var message,
+            destination = ev.params[0],
+            panel = this.app.connections.active_connection.panels.getByName(destination) || this.app.panels().server;
+
+        ev.params.shift();
+        message = ev.params.join(' ');
+
+        panel.addMsg(this.app.connections.active_connection.get('nick'), styleText('privmsg', {text: message}), 'privmsg');
+        this.app.connections.active_connection.gateway.msg(destination, message);
+    }
+
+
+    function actionCommand (ev) {
+        if (this.app.panels().active.isServer()) {
+            return;
+        }
+
+        var panel = this.app.panels().active;
+        panel.addMsg('', styleText('action', {nick: this.app.connections.active_connection.get('nick'), text: ev.params.join(' ')}), 'action');
+        this.app.connections.active_connection.gateway.action(panel.get('name'), ev.params.join(' '));
+    }
+
+
+    function partCommand (ev) {
+        var that = this,
+            chans,
+            msg;
+        if (ev.params.length === 0) {
+            this.app.connections.active_connection.gateway.part(this.app.panels().active.get('name'));
+        } else {
+            chans = ev.params[0].split(',');
+            msg = ev.params[1];
+            _.each(chans, function (channel) {
+                that.connections.active_connection.gateway.part(channel, msg);
+            });
+        }
+    }
+
+
+    function cycleCommand (ev) {
+        var that = this,
+            chan_name;
+
+        if (ev.params.length === 0) {
+            chan_name = this.app.panels().active.get('name');
+        } else {
+            chan_name = ev.params[0];
+        }
+
+        this.app.connections.active_connection.gateway.part(chan_name);
+
+        // Wait for a second to give the network time to register the part command
+        setTimeout(function() {
+            // Use createAndJoinChannels() here as it auto-creates panels instead of waiting for the network
+            that.app.connections.active_connection.createAndJoinChannels(chan_name);
+            that.app.connections.active_connection.panels.getByName(chan_name).show();
+        }, 1000);
+    }
+
+
+    function nickCommand (ev) {
+        this.app.connections.active_connection.gateway.changeNick(ev.params[0]);
+    }
+
+
+    function topicCommand (ev) {
+        var channel_name;
+
+        if (ev.params.length === 0) return;
+
+        if (this.app.connections.active_connection.isChannelName(ev.params[0])) {
+            channel_name = ev.params[0];
+            ev.params.shift();
+        } else {
+            channel_name = this.app.panels().active.get('name');
+        }
+
+        this.app.connections.active_connection.gateway.topic(channel_name, ev.params.join(' '));
+    }
+
+
+    function noticeCommand (ev) {
+        var destination;
+
+        // Make sure we have a destination and some sort of message
+        if (ev.params.length <= 1) return;
+
+        destination = ev.params[0];
+        ev.params.shift();
+
+        this.app.connections.active_connection.gateway.notice(destination, ev.params.join(' '));
+    }
+
+
+    function quoteCommand (ev) {
+        var raw = ev.params.join(' ');
+        this.app.connections.active_connection.gateway.raw(raw);
+    }
+
+
+    function kickCommand (ev) {
+        var nick, panel = this.app.panels().active;
+
+        if (!panel.isChannel()) return;
+
+        // Make sure we have a nick
+        if (ev.params.length === 0) return;
+
+        nick = ev.params[0];
+        ev.params.shift();
+
+        this.app.connections.active_connection.gateway.kick(panel.get('name'), nick, ev.params.join(' '));
+    }
+
+
+    function clearCommand (ev) {
+        // Can't clear a server or applet panel
+        if (this.app.panels().active.isServer() || this.app.panels().active.isApplet()) {
+            return;
+        }
+
+        if (this.app.panels().active.clearMessages) {
+            this.app.panels().active.clearMessages();
+        }
+    }
+
+
+    function ctcpCommand(ev) {
+        var target, type;
+
+        // Make sure we have a target and a ctcp type (eg. version, time)
+        if (ev.params.length < 2) return;
+
+        target = ev.params[0];
+        ev.params.shift();
+
+        type = ev.params[0];
+        ev.params.shift();
+
+        this.app.connections.active_connection.gateway.ctcpRequest(type, target, ev.params.join(' '));
+    }
+
+
+    function settingsCommand (ev) {
+        var settings = _kiwi.model.Applet.loadOnce('kiwi_settings');
+        settings.view.show();
+    }
+
+
+    function scriptCommand (ev) {
+        var editor = _kiwi.model.Applet.loadOnce('kiwi_script_editor');
+        editor.view.show();
+    }
+
+
+    function appletCommand (ev) {
+        if (!ev.params[0]) return;
+
+        var panel = new _kiwi.model.Applet();
+
+        if (ev.params[1]) {
+            // Url and name given
+            panel.load(ev.params[0], ev.params[1]);
+        } else {
+            // Load a pre-loaded applet
+            if (this.applets[ev.params[0]]) {
+                panel.load(new this.applets[ev.params[0]]());
+            } else {
+                this.app.panels().server.addMsg('', styleText('applet_notfound', {text: translateText('client_models_application_applet_notfound', [ev.params[0]])}));
+                return;
+            }
+        }
+
+        this.app.connections.active_connection.panels.add(panel);
+        panel.view.show();
+    }
+
+
+    function inviteCommand (ev) {
+        var nick, channel;
+
+        // A nick must be specified
+        if (!ev.params[0])
+            return;
+
+        // Can only invite into channels
+        if (!this.app.panels().active.isChannel())
+            return;
+
+        nick = ev.params[0];
+        channel = this.app.panels().active.get('name');
+
+        this.app.connections.active_connection.gateway.raw('INVITE ' + nick + ' ' + channel);
+
+        this.app.panels().active.addMsg('', styleText('channel_has_been_invited', {nick: nick, text: translateText('client_models_application_has_been_invited', [channel])}), 'action');
+    }
+
+
+    function whoisCommand (ev) {
+        var nick;
+
+        if (ev.params[0]) {
+            nick = ev.params[0];
+        } else if (this.app.panels().active.isQuery()) {
+            nick = this.app.panels().active.get('name');
+        }
+
+        if (nick)
+            this.app.connections.active_connection.gateway.raw('WHOIS ' + nick + ' ' + nick);
+    }
+
+
+    function whowasCommand (ev) {
+        var nick;
+
+        if (ev.params[0]) {
+            nick = ev.params[0];
+        } else if (this.app.panels().active.isQuery()) {
+            nick = this.app.panels().active.get('name');
+        }
+
+        if (nick)
+            this.app.connections.active_connection.gateway.raw('WHOWAS ' + nick);
+    }
+
+
+    function awayCommand (ev) {
+        this.app.connections.active_connection.gateway.raw('AWAY :' + ev.params.join(' '));
+    }
+
+
+    function encodingCommand (ev) {
+        var that = this;
+
+        if (ev.params[0]) {
+            _kiwi.gateway.setEncoding(null, ev.params[0], function (success) {
+                if (success) {
+                    that.app.panels().active.addMsg('', styleText('encoding_changed', {text: translateText('client_models_application_encoding_changed', [ev.params[0]])}));
+                } else {
+                    that.app.panels().active.addMsg('', styleText('encoding_invalid', {text: translateText('client_models_application_encoding_invalid', [ev.params[0]])}));
+                }
+            });
+        } else {
+            this.app.panels().active.addMsg('', styleText('client_models_application_encoding_notspecified', {text: translateText('client_models_application_encoding_notspecified')}));
+            this.app.panels().active.addMsg('', styleText('client_models_application_encoding_usage', {text: translateText('client_models_application_encoding_usage')}));
+        }
+    }
+
+
+    function channelCommand (ev) {
+        var active_panel = this.app.panels().active;
+
+        if (!active_panel.isChannel())
+            return;
+
+        new _kiwi.model.ChannelInfo({channel: this.app.panels().active});
+    }
+
+
+    function quitCommand (ev) {
+        var network = this.app.connections.active_connection;
+
+        if (!network)
+            return;
+
+        network.gateway.quit(ev.params.join(' '));
+    }
+
+
+    function serverCommand (ev) {
+        var that = this,
+            server, port, ssl, password, nick,
+            tmp;
+
+        // If no server address given, show the new connection dialog
+        if (!ev.params[0]) {
+            tmp = new _kiwi.view.MenuBox(_kiwi.global.i18n.translate('client_models_application_connection_create').fetch());
+            tmp.addItem('new_connection', new _kiwi.model.NewConnection().view.$el);
+            tmp.show();
+
+            // Center screen the dialog
+            tmp.$el.offset({
+                top: (this.app.view.$el.height() / 2) - (tmp.$el.height() / 2),
+                left: (this.app.view.$el.width() / 2) - (tmp.$el.width() / 2)
+            });
+
+            return;
+        }
+
+        // Port given in 'host:port' format and no specific port given after a space
+        if (ev.params[0].indexOf(':') > 0) {
+            tmp = ev.params[0].split(':');
+            server = tmp[0];
+            port = tmp[1];
+
+            password = ev.params[1] || undefined;
+
+        } else {
+            // Server + port given as 'host port'
+            server = ev.params[0];
+            port = ev.params[1] || 6667;
+
+            password = ev.params[2] || undefined;
+        }
+
+        // + in the port means SSL
+        if (port.toString()[0] === '+') {
+            ssl = true;
+            port = parseInt(port.substring(1), 10);
+        } else {
+            ssl = false;
+        }
+
+        // Default port if one wasn't found
+        port = port || 6667;
+
+        // Use the same nick as we currently have
+        nick = this.app.connections.active_connection.get('nick');
+
+        this.app.panels().active.addMsg('', styleText('server_connecting', {text: translateText('client_models_application_connection_connecting', [server, port.toString()])}));
+
+        _kiwi.gateway.newConnection({
+            nick: nick,
+            host: server,
+            port: port,
+            ssl: ssl,
+            password: password
+        }, function(err, new_connection) {
+            var translated_err;
+
+            if (err) {
+                translated_err = translateText('client_models_application_connection_error', [server, port.toString(), err.toString()]);
+                that.app.panels().active.addMsg('', styleText('server_connecting_error', {text: translated_err}));
+            }
+        });
+    }
+
+})();
+
+
+(function () {\r
+    var View = Backbone.View.extend({\r
+        events: {\r
+            'change [data-setting]': 'saveSettings',\r
+            'click [data-setting="theme"]': 'selectTheme',\r
+            'click .register_protocol': 'registerProtocol',\r
+            'click .enable_notifications': 'enableNotifications'\r
+        },\r
+\r
+        initialize: function (options) {\r
+            var text = {\r
+                tabs                  : translateText('client_applets_settings_channelview_tabs'),\r
+                list                  : translateText('client_applets_settings_channelview_list'),\r
+                large_amounts_of_chans: translateText('client_applets_settings_channelview_list_notice'),\r
+                join_part             : translateText('client_applets_settings_notification_joinpart'),\r
+                count_all_activity    : translateText('client_applets_settings_notification_count_all_activity'),\r
+                timestamps            : translateText('client_applets_settings_timestamp'),\r
+                timestamp_24          : translateText('client_applets_settings_timestamp_24_hour'),\r
+                mute                  : translateText('client_applets_settings_notification_sound'),\r
+                emoticons             : translateText('client_applets_settings_emoticons'),\r
+                scroll_history        : translateText('client_applets_settings_history_length'),\r
+                languages             : _kiwi.app.translations,\r
+                default_client        : translateText('client_applets_settings_default_client'),\r
+                make_default          : translateText('client_applets_settings_default_client_enable'),\r
+                locale_restart_needed : translateText('client_applets_settings_locale_restart_needed'),\r
+                default_note          : translateText('client_applets_settings_default_client_notice', '<a href="chrome://settings/handlers">chrome://settings/handlers</a>'),\r
+                html5_notifications   : translateText('client_applets_settings_html5_notifications'),\r
+                enable_notifications  : translateText('client_applets_settings_enable_notifications'),\r
+                theme_thumbnails: _.map(_kiwi.app.themes, function (theme) {\r
+                    return _.template($('#tmpl_theme_thumbnail').html().trim(), theme);\r
+                })\r
+            };\r
+            this.$el = $(_.template($('#tmpl_applet_settings').html().trim(), text));\r
+\r
+            if (!navigator.registerProtocolHandler) {\r
+                this.$('.protocol_handler').remove();\r
+            }\r
+\r
+            if (_kiwi.utils.notifications.allowed() !== null) {\r
+                this.$('.notification_enabler').remove();\r
+            }\r
+\r
+            // Incase any settings change while we have this open, update them\r
+            _kiwi.global.settings.on('change', this.loadSettings, this);\r
+\r
+            // Now actually show the current settings\r
+            this.loadSettings();\r
+\r
+        },\r
+\r
+        loadSettings: function () {\r
+\r
+            _.each(_kiwi.global.settings.attributes, function(value, key) {\r
+\r
+                var $el = this.$('[data-setting="' + key + '"]');\r
+\r
+                // Only deal with settings we have a UI element for\r
+                if (!$el.length)\r
+                    return;\r
+\r
+                switch ($el.prop('type')) {\r
+                    case 'checkbox':\r
+                        $el.prop('checked', value);\r
+                        break;\r
+                    case 'radio':\r
+                        this.$('[data-setting="' + key + '"][value="' + value + '"]').prop('checked', true);\r
+                        break;\r
+                    case 'text':\r
+                        $el.val(value);\r
+                        break;\r
+                    case 'select-one':\r
+                        this.$('[value="' + value + '"]').prop('selected', true);\r
+                        break;\r
+                    default:\r
+                        this.$('[data-setting="' + key + '"][data-value="' + value + '"]').addClass('active');\r
+                        break;\r
+                }\r
+            }, this);\r
+        },\r
+\r
+        saveSettings: function (event) {\r
+            var value,\r
+                settings = _kiwi.global.settings,\r
+                $setting = $(event.currentTarget);\r
+\r
+            switch (event.currentTarget.type) {\r
+                case 'checkbox':\r
+                    value = $setting.is(':checked');\r
+                    break;\r
+                case 'radio':\r
+                case 'text':\r
+                    value = $setting.val();\r
+                    break;\r
+                case 'select-one':\r
+                    value = $(event.currentTarget[$setting.prop('selectedIndex')]).val();\r
+                    break;\r
+                default:\r
+                    value = $setting.data('value');\r
+                    break;\r
+            }\r
+\r
+            // Stop settings being updated while we're saving one by one\r
+            _kiwi.global.settings.off('change', this.loadSettings, this);\r
+            settings.set($setting.data('setting'), value);\r
+            settings.save();\r
+\r
+            // Continue listening for setting changes\r
+            _kiwi.global.settings.on('change', this.loadSettings, this);\r
+        },\r
+\r
+        selectTheme: function(event) {\r
+            event.preventDefault();\r
+\r
+            this.$('[data-setting="theme"].active').removeClass('active');\r
+            $(event.currentTarget).addClass('active').trigger('change');\r
+        },\r
+\r
+        registerProtocol: function (event) {\r
+            event.preventDefault();\r
+\r
+            navigator.registerProtocolHandler('irc', document.location.origin + _kiwi.app.get('base_path') + '/%s', 'Kiwi IRC');\r
+            navigator.registerProtocolHandler('ircs', document.location.origin + _kiwi.app.get('base_path') + '/%s', 'Kiwi IRC');\r
+        },\r
+\r
+        enableNotifications: function(event){\r
+            event.preventDefault();\r
+            var notifications = _kiwi.utils.notifications;\r
+\r
+            notifications.requestPermission().always(_.bind(function () {\r
+                if (notifications.allowed() !== null) {\r
+                    this.$('.notification_enabler').remove();\r
+                }\r
+            }, this));\r
+        }\r
+\r
+    });\r
+\r
+\r
+    var Applet = Backbone.Model.extend({\r
+        initialize: function () {\r
+            this.set('title', translateText('client_applets_settings_title'));\r
+            this.view = new View();\r
+        }\r
+    });\r
+\r
+\r
+    _kiwi.model.Applet.register('kiwi_settings', Applet);\r
+})();\r
+
+
+
+(function () {\r
+\r
+    var View = Backbone.View.extend({\r
+        events: {\r
+            "click .chan": "chanClick",\r
+            "click .channel_name_title": "sortChannelsByNameClick",\r
+            "click .users_title": "sortChannelsByUsersClick"\r
+        },\r
+\r
+\r
+\r
+        initialize: function (options) {\r
+            var text = {\r
+                channel_name: _kiwi.global.i18n.translate('client_applets_chanlist_channelname').fetch(),\r
+                users: _kiwi.global.i18n.translate('client_applets_chanlist_users').fetch(),\r
+                topic: _kiwi.global.i18n.translate('client_applets_chanlist_topic').fetch()\r
+            };\r
+            this.$el = $(_.template($('#tmpl_channel_list').html().trim(), text));\r
+\r
+            this.channels = [];\r
+\r
+            // Sort the table\r
+            this.order = '';\r
+\r
+            // Waiting to add the table back into the DOM?\r
+            this.waiting = false;\r
+        },\r
+\r
+        render: function () {\r
+            var table = $('table', this.$el),\r
+                tbody = table.children('tbody:first').detach(),\r
+                that = this,\r
+                i;\r
+\r
+            // Create the sort icon container and clean previous any previous ones\r
+            if($('.applet_chanlist .users_title').find('span.chanlist_sort_users').length == 0) {\r
+                this.$('.users_title').append('<span class="chanlist_sort_users">&nbsp;&nbsp;</span>');\r
+            } else {\r
+                this.$('.users_title span.chanlist_sort_users').removeClass('fa fa-sort-desc');\r
+                this.$('.users_title span.chanlist_sort_users').removeClass('fa fa-sort-asc');\r
+            }\r
+            if ($('.applet_chanlist .channel_name_title').find('span.chanlist_sort_names').length == 0) {\r
+                this.$('.channel_name_title').append('<span class="chanlist_sort_names">&nbsp;&nbsp;</span>');\r
+            } else {\r
+                this.$('.channel_name_title span.chanlist_sort_names').removeClass('fa fa-sort-desc');\r
+                this.$('.channel_name_title span.chanlist_sort_names').removeClass('fa fa-sort-asc');\r
+            }\r
+\r
+            // Push the new sort icon\r
+            switch (this.order) {\r
+                case 'user_desc':\r
+                default:\r
+                    this.$('.users_title span.chanlist_sort_users').addClass('fa fa-sort-asc');\r
+                    break;\r
+                case 'user_asc':\r
+                    this.$('.users_title span.chanlist_sort_users').addClass('fa fa-sort-desc');\r
+                    break;\r
+                case 'name_asc':\r
+                    this.$('.channel_name_title span.chanlist_sort_names').addClass('fa fa-sort-desc');\r
+                    break;\r
+                case 'name_desc':\r
+                    this.$('.channel_name_title span.chanlist_sort_names').addClass('fa fa-sort-asc');\r
+                    break;\r
+            }\r
+\r
+            this.channels = this.sortChannels(this.channels, this.order);\r
+\r
+            // Make sure all the channel DOM nodes are inserted in order\r
+            for (i = 0; i < this.channels.length; i++) {\r
+                tbody[0].appendChild(this.channels[i].dom);\r
+            }\r
+\r
+            table[0].appendChild(tbody[0]);\r
+        },\r
+\r
+\r
+        chanClick: function (event) {\r
+            if (event.target) {\r
+                _kiwi.gateway.join(null, $(event.target).data('channel'));\r
+            } else {\r
+                // IE...\r
+                _kiwi.gateway.join(null, $(event.srcElement).data('channel'));\r
+            }\r
+        },\r
+\r
+        sortChannelsByNameClick: function (event) {\r
+            // Revert the sorting to switch between orders\r
+            this.order = (this.order == 'name_asc') ? 'name_desc' : 'name_asc';\r
+\r
+            this.sortChannelsClick();\r
+        },\r
+\r
+        sortChannelsByUsersClick: function (event) {\r
+            // Revert the sorting to switch between orders\r
+            this.order = (this.order == 'user_desc' || this.order == '') ? 'user_asc' : 'user_desc';\r
+\r
+            this.sortChannelsClick();\r
+        },\r
+\r
+        sortChannelsClick: function() {\r
+            this.render();\r
+        },\r
+\r
+        sortChannels: function (channels, order) {\r
+            var sort_channels = [],\r
+                new_channels = [];\r
+\r
+\r
+            // First we create a light copy of the channels object to do the sorting\r
+            _.each(channels, function (chan, chan_idx) {\r
+                sort_channels.push({'chan_idx': chan_idx, 'num_users': chan.num_users, 'channel': chan.channel});\r
+            });\r
+\r
+            // Second, we apply the sorting\r
+            sort_channels.sort(function (a, b) {\r
+                switch (order) {\r
+                    case 'user_asc':\r
+                        return a.num_users - b.num_users;\r
+                    case 'user_desc':\r
+                        return b.num_users - a.num_users;\r
+                    case 'name_asc':\r
+                        if (a.channel.toLowerCase() > b.channel.toLowerCase()) return 1;\r
+                        if (a.channel.toLowerCase() < b.channel.toLowerCase()) return -1;\r
+                    case 'name_desc':\r
+                        if (a.channel.toLowerCase() < b.channel.toLowerCase()) return 1;\r
+                        if (a.channel.toLowerCase() > b.channel.toLowerCase()) return -1;\r
+                    default:\r
+                        return b.num_users - a.num_users;\r
+                }\r
+                return 0;\r
+            });\r
+\r
+            // Third, we re-shuffle the chanlist according to the sort order\r
+            _.each(sort_channels, function (chan) {\r
+                new_channels.push(channels[chan.chan_idx]);\r
+            });\r
+\r
+            return new_channels;\r
+        }\r
+    });\r
+\r
+\r
+\r
+    var Applet = Backbone.Model.extend({\r
+        initialize: function () {\r
+            this.set('title', _kiwi.global.i18n.translate('client_applets_chanlist_channellist').fetch());\r
+            this.view = new View();\r
+\r
+            this.network = _kiwi.global.components.Network();\r
+            this.network.on('list_channel', this.onListChannel, this);\r
+            this.network.on('list_start', this.onListStart, this);\r
+        },\r
+\r
+\r
+        // New channels to add to our list\r
+        onListChannel: function (event) {\r
+            this.addChannel(event.chans);\r
+        },\r
+\r
+        // A new, fresh channel list starting\r
+        onListStart: function (event) {\r
+            // TODO: clear out our existing list\r
+        },\r
+\r
+        addChannel: function (channels) {\r
+            var that = this;\r
+\r
+            if (!_.isArray(channels)) {\r
+                channels = [channels];\r
+            }\r
+            _.each(channels, function (chan) {\r
+                var row;\r
+                row = document.createElement("tr");\r
+                row.innerHTML = '<td class="chanlist_name"><a class="chan" data-channel="' + chan.channel + '">' + _.escape(chan.channel) + '</a></td><td class="chanlist_num_users" style="text-align: center;">' + chan.num_users + '</td><td style="padding-left: 2em;" class="chanlist_topic">' + formatIRCMsg(_.escape(chan.topic)) + '</td>';\r
+                chan.dom = row;\r
+                that.view.channels.push(chan);\r
+            });\r
+\r
+            if (!that.view.waiting) {\r
+                that.view.waiting = true;\r
+                _.defer(function () {\r
+                    that.view.render();\r
+                    that.view.waiting = false;\r
+                });\r
+            }\r
+        },\r
+\r
+\r
+        dispose: function () {\r
+            this.view.channels = null;\r
+            this.view.unbind();\r
+            this.view.$el.html('');\r
+            this.view.remove();\r
+            this.view = null;\r
+\r
+            // Remove any network event bindings\r
+            this.network.off();\r
+        }\r
+    });\r
+\r
+\r
+\r
+    _kiwi.model.Applet.register('kiwi_chanlist', Applet);\r
+})();
+
+
+    (function () {
+        var view = Backbone.View.extend({
+            events: {
+                'click .btn_save': 'onSave'
+            },
+
+            initialize: function (options) {
+                var that = this,
+                    text = {
+                        save: _kiwi.global.i18n.translate('client_applets_scripteditor_save').fetch()
+                    };
+                this.$el = $(_.template($('#tmpl_script_editor').html().trim(), text));
+
+                this.model.on('applet_loaded', function () {
+                    that.$el.parent().css('height', '100%');
+                    $script(_kiwi.app.get('base_path') + '/assets/libs/ace/ace.js', function (){ that.createAce(); });
+                });
+            },
+
+
+            createAce: function () {
+                var editor_id = 'editor_' + Math.floor(Math.random()*10000000).toString();
+                this.editor_id = editor_id;
+
+                this.$el.find('.editor').attr('id', editor_id);
+
+                this.editor = ace.edit(editor_id);
+                this.editor.setTheme("ace/theme/monokai");
+                this.editor.getSession().setMode("ace/mode/javascript");
+
+                var script_content = _kiwi.global.settings.get('user_script') || '';
+                this.editor.setValue(script_content);
+            },
+
+
+            onSave: function (event) {
+                var script_content, user_fn;
+
+                // Build the user script up with some pre-defined components
+                script_content = 'var network = kiwi.components.Network();\n';
+                script_content += 'var input = kiwi.components.ControlInput();\n';
+                script_content += 'var events = kiwi.components.Events();\n';
+                script_content += this.editor.getValue() + '\n';
+
+                // Add a dispose method to the user script for cleaning up
+                script_content += 'this._dispose = function(){ network.off(); input.off(); events.dispose(); if(this.dispose) this.dispose(); }';
+
+                // Try to compile the user script
+                try {
+                    user_fn = new Function(script_content);
+
+                    // Dispose any existing user script
+                    if (_kiwi.user_script && _kiwi.user_script._dispose)
+                        _kiwi.user_script._dispose();
+
+                    // Create and run the new user script
+                    _kiwi.user_script = new user_fn();
+
+                } catch (err) {
+                    this.setStatus(_kiwi.global.i18n.translate('client_applets_scripteditor_error').fetch(err.toString()));
+                    return;
+                }
+
+                // If we're this far, no errors occured. Save the user script
+                _kiwi.global.settings.set('user_script', this.editor.getValue());
+                _kiwi.global.settings.save();
+
+                this.setStatus(_kiwi.global.i18n.translate('client_applets_scripteditor_saved').fetch() + ' :)');
+            },
+
+
+            setStatus: function (status_text) {
+                var $status = this.$el.find('.toolbar .status');
+
+                status_text = status_text || '';
+                $status.slideUp('fast', function() {
+                    $status.text(status_text);
+                    $status.slideDown();
+                });
+            }
+        });
+
+
+
+        var applet = Backbone.Model.extend({
+            initialize: function () {
+                var that = this;
+
+                this.set('title', _kiwi.global.i18n.translate('client_applets_scripteditor_title').fetch());
+                this.view = new view({model: this});
+
+            }
+        });
+
+
+        _kiwi.model.Applet.register('kiwi_script_editor', applet);
+        //_kiwi.model.Applet.loadOnce('kiwi_script_editor');
+    })();
+
+
+(function () {
+    var view = Backbone.View.extend({
+        events: {},
+
+
+        initialize: function (options) {
+            this.showConnectionDialog();
+        },
+
+
+        showConnectionDialog: function() {
+            var connection_dialog = this.connection_dialog = new _kiwi.model.NewConnection();
+            connection_dialog.populateDefaultServerSettings();
+
+            connection_dialog.view.$el.addClass('initial');
+            this.$el.append(connection_dialog.view.$el);
+
+            var $info = $($('#tmpl_new_connection_info').html().trim());
+
+            if ($info.html()) {
+                connection_dialog.view.infoBoxSet($info);
+            } else {
+                $info = null;
+            }
+
+            this.listenTo(connection_dialog, 'connected', this.newConnectionConnected);
+
+            _.defer(function(){
+                if ($info) {
+                    connection_dialog.view.infoBoxShow();
+                }
+
+                // Only set focus if we're not within an iframe. (firefox auto scrolls to the embedded client on page load - bad)
+                if (window == window.top) {
+                    connection_dialog.view.$el.find('.nick').select();
+                }
+            });
+        },
+
+
+        newConnectionConnected: function(network) {
+            // Once connected, reset the connection form to be used again in future
+            this.connection_dialog.view.reset();
+        }
+    });
+
+
+
+    var applet = Backbone.Model.extend({
+        initialize: function () {
+            this.view = new view({model: this});
+        }
+    });
+
+
+    _kiwi.model.Applet.register('kiwi_startup', applet);
+})();
+
+
+
+_kiwi.utils.notifications = (function () {
+    if (!window.Notification) {
+        return {
+            allowed: _.constant(false),
+            requestPermission: _.constant($.Deferred().reject())
+        };
+    }
+
+    var notifications = {
+        /**
+         * Check if desktop notifications have been allowed by the user.
+         *
+         * @returns {?Boolean} `true`  - they have been allowed.
+         *                     `false` - they have been blocked.
+         *                     `null`  - the user hasn't answered yet.
+         */
+        allowed: function () {
+            return Notification.permission === 'granted' ? true
+                 : Notification.permission === 'denied' ? false
+                 : null;
+        },
+
+        /**
+         * Ask the user their permission to display desktop notifications.
+         * This will return a promise which will be resolved if the user allows notifications, or rejected if they blocked
+         * notifictions or simply closed the dialog. If the user had previously given their preference, the promise will be
+         * immediately resolved or rejected with their previous answer.
+         *
+         * @example
+         *   notifications.requestPermission().then(function () { 'allowed' }, function () { 'not allowed' });
+         *
+         * @returns {Promise}
+         */
+        requestPermission: function () {
+            var deferred = $.Deferred();
+            Notification.requestPermission(function (permission) {
+                deferred[(permission === 'granted') ? 'resolve' : 'reject']();
+            });
+            return deferred.promise();
+        },
+
+        /**
+         * Create a new notification. If the user has not yet given permission to display notifications, they will be asked
+         * to confirm first. The notification will show afterwards if they allow it.
+         *
+         * Notifications implement Backbone.Events (so you can use `on` and `off`). They trigger four different events:
+         *   - 'click'
+         *   - 'close'
+         *   - 'error'
+         *   - 'show'
+         *
+         * @example
+         *   notifications
+         *     .create('Cool notification', { icon: 'logo.png' })
+         *     .on('click', function () {
+         *       window.focus();
+         *     })
+         *     .closeAfter(5000);
+         *
+         * @param   {String}  title
+         * @param   {Object}  options
+         * @param   {String=} options.body  A string representing an extra content to display within the notification
+         * @param   {String=} options.dir   The direction of the notification; it can be auto, ltr, or rtl
+         * @param   {String=} options.lang  Specify the lang used within the notification. This string must be a valid BCP
+         *                                  47 language tag.
+         * @param   {String=} options.tag   An ID for a given notification that allows to retrieve, replace or remove it if necessary
+         * @param   {String=} options.icon  The URL of an image to be used as an icon by the notification
+         * @returns {Notifier}
+         */
+        create: function (title, options) {
+            return new Notifier(title, options);
+        }
+    };
+
+    function Notifier(title, options) {
+        createNotification.call(this, title, options);
+    }
+    _.extend(Notifier.prototype, Backbone.Events, {
+        closed: false,
+        _closeTimeout: null,
+
+        /**
+         * Close the notification after a given number of milliseconds.
+         * @param   {Number} timeout
+         * @returns {this}
+         */
+        closeAfter: function (timeout) {
+            if (!this.closed) {
+                if (this.notification) {
+                    this._closeTimeout = this._closeTimeout || setTimeout(_.bind(this.close, this), timeout);
+                } else {
+                    this.once('show', _.bind(this.closeAfter, this, timeout));
+                }
+            }
+            return this;
+        },
+
+        /**
+         * Close the notification immediately.
+         * @returns {this}
+         */
+        close: function () {
+            if (this.notification && !this.closed) {
+                this.notification.close();
+                this.closed = true;
+            }
+            return this;
+        }
+    });
+
+    function createNotification(title, options) {
+        switch (notifications.allowed()) {
+            case true:
+                this.notification = new Notification(title, options);
+                _.each(['click', 'close', 'error', 'show'], function (eventName) {
+                    this.notification['on' + eventName] = _.bind(this.trigger, this, eventName);
+                }, this);
+                break;
+            case null:
+                notifications.requestPermission().done(_.bind(createNotification, this, title, options));
+                break;
+        }
+    }
+
+    return notifications;
+}());
+
+
+
+_kiwi.utils.formatDate = (function() {
+    /*
+    Modified version of date.format.js
+    https://github.com/jacwright/date.format
+    */
+    var locale_init = false, // Once the loales have been loaded, this is set to true
+        shortMonths, longMonths, shortDays, longDays;
+
+    // defining patterns
+    var replaceChars = {
+        // Day
+        d: function() { return (this.getDate() < 10 ? '0' : '') + this.getDate(); },
+        D: function() { return Date.shortDays[this.getDay()]; },
+        j: function() { return this.getDate(); },
+        l: function() { return Date.longDays[this.getDay()]; },
+        N: function() { return this.getDay() + 1; },
+        S: function() { return (this.getDate() % 10 == 1 && this.getDate() != 11 ? 'st' : (this.getDate() % 10 == 2 && this.getDate() != 12 ? 'nd' : (this.getDate() % 10 == 3 && this.getDate() != 13 ? 'rd' : 'th'))); },
+        w: function() { return this.getDay(); },
+        z: function() { var d = new Date(this.getFullYear(),0,1); return Math.ceil((this - d) / 86400000); }, // Fixed now
+        // Week
+        W: function() { var d = new Date(this.getFullYear(), 0, 1); return Math.ceil((((this - d) / 86400000) + d.getDay() + 1) / 7); }, // Fixed now
+        // Month
+        F: function() { return Date.longMonths[this.getMonth()]; },
+        m: function() { return (this.getMonth() < 9 ? '0' : '') + (this.getMonth() + 1); },
+        M: function() { return Date.shortMonths[this.getMonth()]; },
+        n: function() { return this.getMonth() + 1; },
+        t: function() { var d = new Date(); return new Date(d.getFullYear(), d.getMonth(), 0).getDate(); }, // Fixed now, gets #days of date
+        // Year
+        L: function() { var year = this.getFullYear(); return (year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)); },   // Fixed now
+        o: function() { var d  = new Date(this.valueOf());  d.setDate(d.getDate() - ((this.getDay() + 6) % 7) + 3); return d.getFullYear();}, //Fixed now
+        Y: function() { return this.getFullYear(); },
+        y: function() { return ('' + this.getFullYear()).substr(2); },
+        // Time
+        a: function() { return this.getHours() < 12 ? 'am' : 'pm'; },
+        A: function() { return this.getHours() < 12 ? 'AM' : 'PM'; },
+        B: function() { return Math.floor((((this.getUTCHours() + 1) % 24) + this.getUTCMinutes() / 60 + this.getUTCSeconds() / 3600) * 1000 / 24); }, // Fixed now
+        g: function() { return this.getHours() % 12 || 12; },
+        G: function() { return this.getHours(); },
+        h: function() { return ((this.getHours() % 12 || 12) < 10 ? '0' : '') + (this.getHours() % 12 || 12); },
+        H: function() { return (this.getHours() < 10 ? '0' : '') + this.getHours(); },
+        i: function() { return (this.getMinutes() < 10 ? '0' : '') + this.getMinutes(); },
+        s: function() { return (this.getSeconds() < 10 ? '0' : '') + this.getSeconds(); },
+        u: function() { var m = this.getMilliseconds(); return (m < 10 ? '00' : (m < 100 ? '0' : '')) + m; },
+        // Timezone
+        e: function() { return "Not Yet Supported"; },
+        I: function() {
+            var DST = null;
+                for (var i = 0; i < 12; ++i) {
+                        var d = new Date(this.getFullYear(), i, 1);
+                        var offset = d.getTimezoneOffset();
+
+                        if (DST === null) DST = offset;
+                        else if (offset < DST) { DST = offset; break; }
+                        else if (offset > DST) break;
+                }
+                return (this.getTimezoneOffset() == DST) | 0;
+            },
+        O: function() { return (-this.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(this.getTimezoneOffset() / 60)) + '00'; },
+        P: function() { return (-this.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(this.getTimezoneOffset() / 60)) + ':00'; }, // Fixed now
+        T: function() { var m = this.getMonth(); this.setMonth(0); var result = this.toTimeString().replace(/^.+ \(?([^\)]+)\)?$/, '$1'); this.setMonth(m); return result;},
+        Z: function() { return -this.getTimezoneOffset() * 60; },
+        // Full Date/Time
+        c: function() { return this.format("Y-m-d\\TH:i:sP"); }, // Fixed now
+        r: function() { return this.toString(); },
+        U: function() { return this.getTime() / 1000; }
+    };
+
+
+    var initLocaleFormats = function() {
+        shortMonths = [
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.january').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.february').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.march').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.april').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.may').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.june').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.july').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.august').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.september').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.october').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.november').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_months.december').fetch()
+        ];
+        longMonths = [
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.january').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.february').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.march').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.april').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.may').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.june').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.july').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.august').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.september').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.october').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.november').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_months.december').fetch()
+        ];
+        shortDays = [
+            _kiwi.global.i18n.translate('client.libs.date_format.short_days.monday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_days.tuesday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_days.wednesday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_days.thursday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_days.friday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_days.saturday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.short_days.sunday').fetch()
+        ];
+        longDays = [
+            _kiwi.global.i18n.translate('client.libs.date_format.long_days.monday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_days.tuesday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_days.wednesday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_days.thursday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_days.friday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_days.saturday').fetch(),
+            _kiwi.global.i18n.translate('client.libs.date_format.long_days.sunday').fetch()
+        ];
+
+        locale_init = true;
+    };
+    /* End of date.format */
+
+
+    // Finally.. the actuall formatDate function
+    return function(working_date, format) {
+        if (!locale_init)
+            initLocaleFormats();
+
+        working_date = working_date || new Date();
+        format = format || _kiwi.global.i18n.translate('client_date_format').fetch();
+
+        return format.replace(/(\\?)(.)/g, function(_, esc, chr) {
+            return (esc === '' && replaceChars[chr]) ? replaceChars[chr].call(working_date) : chr;
+        });
+    };
+})();
+
+
+/*
+ * The same functionality as EventEmitter but with the inclusion of callbacks
+ */
+
+
+
+function PluginInterface () {
+    // Holder for all the bound listeners by this module
+    this._listeners = {};
+
+    // Event proxies
+    this._parent = null;
+    this._children = [];
+}
+
+
+
+PluginInterface.prototype.on = function (event_name, fn, scope) {
+    this._listeners[event_name] = this._listeners[event_name] || [];
+    this._listeners[event_name].push(['on', fn, scope]);
+};
+
+
+
+PluginInterface.prototype.once = function (event_name, fn, scope) {
+    this._listeners[event_name] = this._listeners[event_name] || [];
+    this._listeners[event_name].push(['once', fn, scope]);
+};
+
+
+
+PluginInterface.prototype.off = function (event_name, fn, scope) {
+    var idx;
+
+    if (typeof event_name === 'undefined') {
+        // Remove all listeners
+        this._listeners = {};
+
+    } else if (typeof fn === 'undefined') {
+        // Remove all of 1 event type
+        delete this._listeners[event_name];
+
+    } else if (typeof scope === 'undefined') {
+        // Remove a single event type + callback
+        for (idx in (this._listeners[event_name] || [])) {
+            if (this._listeners[event_name][idx][1] === fn) {
+                delete this._listeners[event_name][idx];
+            }
+        }
+    } else {
+        // Remove a single event type + callback + scope
+        for (idx in (this._listeners[event_name] || [])) {
+            if (this._listeners[event_name][idx][1] === fn && this._listeners[event_name][idx][2] === scope) {
+                delete this._listeners[event_name][idx];
+            }
+        }
+    }
+};
+
+
+
+PluginInterface.prototype.getListeners = function(event_name) {
+    return this._listeners[event_name] || [];
+};
+
+
+
+PluginInterface.prototype.createProxy = function() {
+    var proxy = new PluginInterface();
+    proxy._parent = this._parent || this;
+    proxy._parent._children.push(proxy);
+
+    return proxy;
+};
+
+
+
+PluginInterface.prototype.dispose = function() {
+    this.off();
+
+    if (this._parent) {
+        var idx = this._parent._children.indexOf(this);
+        if (idx > -1) {
+            this._parent._children.splice(idx, 1);
+        }
+    }
+};
+
+
+
+// Call all the listeners for a certain event, passing them some event data that may be changed
+PluginInterface.prototype.emit = function (event_name, event_data) {
+    var emitter = new this.EmitCall(event_name, event_data),
+        listeners = [],
+        child_idx;
+
+    // Get each childs event listeners in order of last created
+    for(child_idx=this._children.length-1; child_idx>=0; child_idx--) {
+        listeners = listeners.concat(this._children[child_idx].getListeners(event_name));
+    }
+
+    // Now include any listeners directly on this instance
+    listeners = listeners.concat(this.getListeners(event_name));
+
+    // Once emitted, remove any 'once' bound listeners
+    emitter.then(function () {
+        var len = listeners.length,
+            idx;
+
+        for(idx = 0; idx < len; idx++) {
+            if (listeners[idx][0] === 'once') {
+                listeners[idx] = undefined;
+            }
+        }
+    });
+
+    // Emit the event to the listeners and return
+    emitter.callListeners(listeners);
+    return emitter;
+};
+
+
+
+// Promise style object to emit events to listeners
+PluginInterface.prototype.EmitCall = function EmitCall (event_name, event_data) {
+    var that = this,
+        completed = false,
+        completed_fn = [],
+
+        // Has event.preventDefault() been called
+        prevented = false,
+        prevented_fn = [];
+
+
+    // Emit this event to an array of listeners
+    function callListeners(listeners) {
+        var current_event_idx = -1;
+
+        // Make sure we have some data to pass to the listeners
+        event_data = event_data || undefined;
+
+        // If no bound listeners for this event, leave now
+        if (listeners.length === 0) {
+            emitComplete();
+            return;
+        }
+
+
+        // Call the next listener in our array
+        function nextListener() {
+            var listener, event_obj;
+
+            // We want the next listener
+            current_event_idx++;
+
+            // If we've ran out of listeners end this emit call
+            if (!listeners[current_event_idx]) {
+                emitComplete();
+                return;
+            }
+
+            // Object the listener ammends to tell us what it's going to do
+            event_obj = {
+                // If changed to true, expect this listener is going to callback
+                wait: false,
+
+                // If wait is true, this callback must be called to continue running listeners
+                callback: function () {
+                    // Invalidate this callback incase a listener decides to call it again
+                    event_obj.callback = undefined;
+
+                    nextListener.apply(that);
+                },
+
+                // Prevents the default 'done' functions from executing
+                preventDefault: function () {
+                    prevented = true;
+                }
+            };
+
+
+            listener = listeners[current_event_idx];
+            listener[1].call(listener[2] || that, event_obj, event_data);
+
+            // If the listener hasn't signalled it's going to wait, proceed to next listener
+            if (!event_obj.wait) {
+                // Invalidate the callback just incase a listener decides to call it anyway
+                event_obj.callback = undefined;
+
+                nextListener();
+            }
+        }
+
+        nextListener();
+    }
+
+
+
+    function emitComplete() {
+        completed = true;
+
+        var funcs = prevented ? prevented_fn : completed_fn;
+        funcs = funcs || [];
+
+        // Call the completed/prevented functions
+        for (var idx = 0; idx < funcs.length; idx++) {
+            if (typeof funcs[idx] === 'function') funcs[idx]();
+        }
+    }
+
+
+
+    function addCompletedFunc(fn) {
+        // Only accept functions
+        if (typeof fn !== 'function') return false;
+
+        completed_fn.push(fn);
+
+        // If we have already completed the emits, call this now
+        if (completed && !prevented) fn();
+
+        return this;
+    }
+
+
+
+    function addPreventedFunc(fn) {
+        // Only accept functions
+        if (typeof fn !== 'function') return false;
+
+        prevented_fn.push(fn);
+
+        // If we have already completed the emits, call this now
+        if (completed && prevented) fn();
+
+        return this;
+    }
+
+
+    return {
+        callListeners: callListeners,
+        then: addCompletedFunc,
+        catch: addPreventedFunc
+    };
+};
+
+
+
+// If running a node module, set the exports
+if (typeof module === 'object' && typeof module.exports !== 'undefined') {
+    module.exports = PluginInterface;
+}
+
+
+
+/*
+ * Example usage
+ */
+
+
+/*
+var modules = new PluginInterface();
+
+
+
+// A plugin
+modules.on('irc message', function (event, data) {
+    //event.wait = true;
+    setTimeout(event.callback, 2000);
+});
+
+
+
+
+// Core code that is being extended by plugins
+var data = {
+    nick: 'prawnsalald',
+    command: '/dothis'
+};
+
+modules.emit('irc message', data).done(function () {
+    console.log('Your command is: ' + data.command);
+});
+*/
+
+
+/*jslint devel: true, browser: true, continue: true, sloppy: true, forin: true, plusplus: true, maxerr: 50, indent: 4, nomen: true, regexp: true*/
+/*globals $, front, gateway, Utilityview */
+
+
+
+/**
+*   Generate a random string of given length
+*   @param      {Number}    string_length   The length of the random string
+*   @returns    {String}                    The random string
+*/
+function randomString(string_length) {
+    var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz",
+        randomstring = '',
+        i,
+        rnum;
+    for (i = 0; i < string_length; i++) {
+        rnum = Math.floor(Math.random() * chars.length);
+        randomstring += chars.substring(rnum, rnum + 1);
+    }
+    return randomstring;
+}
+
+/**
+*   String.trim shim
+*/
+if (typeof String.prototype.trim === 'undefined') {
+    String.prototype.trim = function () {
+        return this.replace(/^\s+|\s+$/g, "");
+    };
+}
+
+/**
+*   String.lpad shim
+*   @param      {Number}    length      The length of padding
+*   @param      {String}    characher   The character to pad with
+*   @returns    {String}                The padded string
+*/
+if (typeof String.prototype.lpad === 'undefined') {
+    String.prototype.lpad = function (length, character) {
+        var padding = "",
+            i;
+        for (i = 0; i < length; i++) {
+            padding += character;
+        }
+        return (padding + this).slice(-length);
+    };
+}
+
+
+/**
+*   Convert seconds into hours:minutes:seconds
+*   @param      {Number}    secs    The number of seconds to converts
+*   @returns    {Object}            An object representing the hours/minutes/second conversion of secs
+*/
+function secondsToTime(secs) {
+    var hours, minutes, seconds, divisor_for_minutes, divisor_for_seconds, obj;
+    hours = Math.floor(secs / (60 * 60));
+
+    divisor_for_minutes = secs % (60 * 60);
+    minutes = Math.floor(divisor_for_minutes / 60);
+
+    divisor_for_seconds = divisor_for_minutes % 60;
+    seconds = Math.ceil(divisor_for_seconds);
+
+    obj = {
+        "h": hours,
+        "m": minutes,
+        "s": seconds
+    };
+    return obj;
+}
+
+
+/* Command input Alias + re-writing */
+function InputPreProcessor () {
+    this.recursive_depth = 3;
+
+    this.aliases = {};
+    this.vars = {version: 1};
+
+    // Current recursive depth
+    var depth = 0;
+
+
+    // Takes an array of words to process!
+    this.processInput = function (input) {
+        var words = input || [],
+            alias = this.aliases[words[0]],
+            alias_len,
+            current_alias_word = '',
+            compiled = [];
+
+        // If an alias wasn't found, return the original input
+        if (!alias) return input;
+
+        // Split the alias up into useable words
+        alias = alias.split(' ');
+        alias_len = alias.length;
+
+        // Iterate over each word and pop them into the final compiled array.
+        // Any $ words are processed with the result ending into the compiled array.
+        for (var i=0; i<alias_len; i++) {
+            current_alias_word = alias[i];
+
+            // Non $ word
+            if (current_alias_word[0] !== '$') {
+                compiled.push(current_alias_word);
+                continue;
+            }
+
+            // Refering to an input word ($N)
+            if (!isNaN(current_alias_word[1])) {
+                var num = current_alias_word.match(/\$(\d+)(\+)?(\d+)?/);
+
+                // Did we find anything or does the word it refers to non-existant?
+                if (!num || !words[num[1]]) continue;
+
+                if (num[2] === '+' && num[3]) {
+                    // Add X number of words
+                    compiled = compiled.concat(words.slice(parseInt(num[1], 10), parseInt(num[1], 10) + parseInt(num[3], 10)));
+                } else if (num[2] === '+') {
+                    // Add the remaining of the words
+                    compiled = compiled.concat(words.slice(parseInt(num[1], 10)));
+                } else {
+                    // Add a single word
+                    compiled.push(words[parseInt(num[1], 10)]);
+                }
+
+                continue;
+            }
+
+
+            // Refering to a variable
+            if (typeof this.vars[current_alias_word.substr(1)] !== 'undefined') {
+
+                // Get the variable
+                compiled.push(this.vars[current_alias_word.substr(1)]);
+
+                continue;
+            }
+
+        }
+
+        return compiled;
+    };
+
+
+    this.process = function (input) {
+        input = input || '';
+
+        var words = input.split(' ');
+
+        depth++;
+        if (depth >= this.recursive_depth) {
+            depth--;
+            return input;
+        }
+
+        if (this.aliases[words[0]]) {
+            words = this.processInput(words);
+
+            if (this.aliases[words[0]]) {
+                words = this.process(words.join(' ')).split(' ');
+            }
+
+        }
+
+        depth--;
+        return words.join(' ');
+    };
+}
+
+
+/**
+ * Convert HSL to RGB formatted colour
+ */
+function hsl2rgb(h, s, l) {
+    var m1, m2, hue;
+    var r, g, b
+    s /=100;
+    l /= 100;
+    if (s == 0)
+        r = g = b = (l * 255);
+    else {
+        function HueToRgb(m1, m2, hue) {
+            var v;
+            if (hue < 0)
+                hue += 1;
+            else if (hue > 1)
+                hue -= 1;
+
+            if (6 * hue < 1)
+                v = m1 + (m2 - m1) * hue * 6;
+            else if (2 * hue < 1)
+                v = m2;
+            else if (3 * hue < 2)
+                v = m1 + (m2 - m1) * (2/3 - hue) * 6;
+            else
+                v = m1;
+
+            return 255 * v;
+        }
+        if (l <= 0.5)
+            m2 = l * (s + 1);
+        else
+            m2 = l + s - l * s;
+        m1 = l * 2 - m2;
+        hue = h / 360;
+        r = HueToRgb(m1, m2, hue + 1/3);
+        g = HueToRgb(m1, m2, hue);
+        b = HueToRgb(m1, m2, hue - 1/3);
+    }
+    return [r,g,b];
+}
+
+
+/**
+ * Formats a kiwi message to IRC format
+ */
+function formatToIrcMsg(message) {
+    // Format any colour codes (eg. $c4)
+    message = message.replace(/%C(\d)/g, function(match, colour_number) {
+        return String.fromCharCode(3) + colour_number.toString();
+    });
+
+    var formatters = {
+        B: '\x02',    // Bold
+        I: '\x1D',    // Italics
+        U: '\x1F',    // Underline
+        O: '\x0F'     // Out / Clear formatting
+    };
+    message = message.replace(/%([BIUO])/g, function(match, format_code) {
+        if (typeof formatters[format_code.toUpperCase()] !== 'undefined')
+            return formatters[format_code.toUpperCase()];
+    });
+
+    return message;
+}
+
+
+/**
+*   Formats a message. Adds bold, underline and colouring
+*   @param      {String}    msg The message to format
+*   @returns    {String}        The HTML formatted message
+*/
+function formatIRCMsg (msg) {
+    "use strict";
+    var out = '',
+        currentTag = '',
+        openTags = {
+            bold: false,
+            italic: false,
+            underline: false,
+            colour: false
+        },
+        spanFromOpen = function () {
+            var style = '',
+                colours;
+            if (!(openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
+                return '';
+            } else {
+                style += (openTags.bold) ? 'font-weight: bold; ' : '';
+                style += (openTags.italic) ? 'font-style: italic; ' : '';
+                style += (openTags.underline) ? 'text-decoration: underline; ' : '';
+                if (openTags.colour) {
+                    colours = openTags.colour.split(',');
+                    style += 'color: ' + colours[0] + ((colours[1]) ? '; background-color: ' + colours[1] + ';' : '');
+                }
+                return '<span class="format_span" style="' + style + '">';
+            }
+        },
+        colourMatch = function (str) {
+            var re = /^\x03(([0-9][0-9]?)(,([0-9][0-9]?))?)/;
+            return re.exec(str);
+        },
+        hexFromNum = function (num) {
+            switch (parseInt(num, 10)) {
+            case 0:
+                return '#FFFFFF';
+            case 1:
+                return '#000000';
+            case 2:
+                return '#000080';
+            case 3:
+                return '#008000';
+            case 4:
+                return '#FF0000';
+            case 5:
+                return '#800040';
+            case 6:
+                return '#800080';
+            case 7:
+                return '#FF8040';
+            case 8:
+                return '#FFFF00';
+            case 9:
+                return '#80FF00';
+            case 10:
+                return '#008080';
+            case 11:
+                return '#00FFFF';
+            case 12:
+                return '#0000FF';
+            case 13:
+                return '#FF55FF';
+            case 14:
+                return '#808080';
+            case 15:
+                return '#C0C0C0';
+            default:
+                return null;
+            }
+        },
+        i = 0,
+        colours = [],
+        match;
+
+    for (i = 0; i < msg.length; i++) {
+        switch (msg[i]) {
+        case '\x02':
+            if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
+                out += currentTag + '</span>';
+            }
+            openTags.bold = !openTags.bold;
+            currentTag = spanFromOpen();
+            break;
+        case '\x1D':
+            if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
+                out += currentTag + '</span>';
+            }
+            openTags.italic = !openTags.italic;
+            currentTag = spanFromOpen();
+            break;
+        case '\x1F':
+            if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
+                out += currentTag + '</span>';
+            }
+            openTags.underline = !openTags.underline;
+            currentTag = spanFromOpen();
+            break;
+        case '\x03':
+            if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
+                out += currentTag + '</span>';
+            }
+            match = colourMatch(msg.substr(i, 6));
+            if (match) {
+                i += match[1].length;
+                // 2 & 4
+                colours[0] = hexFromNum(match[2]);
+                if (match[4]) {
+                    colours[1] = hexFromNum(match[4]);
+                }
+                openTags.colour = colours.join(',');
+            } else {
+                openTags.colour = false;
+            }
+            currentTag = spanFromOpen();
+            break;
+        case '\x0F':
+            if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
+                out += currentTag + '</span>';
+            }
+            openTags.bold = openTags.italic = openTags.underline = openTags.colour = false;
+            break;
+        default:
+            if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
+                currentTag += msg[i];
+            } else {
+                out += msg[i];
+            }
+            break;
+        }
+    }
+    if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
+        out += currentTag + '</span>';
+    }
+    return out;
+}
+
+function escapeRegex (str) {
+    return str.replace(/[\[\\\^\$\.\|\?\*\+\(\)]/g, '\\$&');
+}
+
+function emoticonFromText(str) {
+    var words_in = str.split(' '),
+        words_out = [],
+        i,
+        pushEmoticon = function (alt, emote_name) {
+            words_out.push('<i class="emoticon ' + emote_name + '">' + alt + '</i>');
+        };
+
+    for (i = 0; i < words_in.length; i++) {
+        switch(words_in[i]) {
+        case ':)':
+            pushEmoticon(':)', 'smile');
+            break;
+        case ':(':
+            pushEmoticon(':(', 'sad');
+            break;
+        case ':3':
+            pushEmoticon(':3', 'lion');
+            break;
+        case ';3':
+            pushEmoticon(';3', 'winky_lion');
+            break;
+        case ':s':
+        case ':S':
+            pushEmoticon(':s', 'confused');
+            break;
+        case ';(':
+        case ';_;':
+            pushEmoticon(';(', 'cry');
+            break;
+        case ';)':
+            pushEmoticon(';)', 'wink');
+            break;
+        case ';D':
+            pushEmoticon(';D', 'wink_happy');
+            break;
+        case ':P':
+        case ':p':
+            pushEmoticon(':P', 'tongue');
+            break;
+        case 'xP':
+            pushEmoticon('xP', 'cringe_tongue');
+            break;
+        case ':o':
+        case ':O':
+        case ':0':
+            pushEmoticon(':o', 'shocked');
+            break;
+        case ':D':
+            pushEmoticon(':D', 'happy');
+            break;
+        case '^^':
+        case '^.^':
+            pushEmoticon('^^,', 'eyebrows');
+            break;
+        case '&lt;3':
+            pushEmoticon('<3', 'heart');
+            break;
+        case '&gt;_&lt;':
+        case '&gt;.&lt;':
+            pushEmoticon('>_<', 'doh');
+            break;
+        case 'XD':
+        case 'xD':
+            pushEmoticon('xD', 'big_grin');
+            break;
+        case 'o.0':
+        case 'o.O':
+            pushEmoticon('o.0', 'wide_eye_right');
+            break;
+        case '0.o':
+        case 'O.o':
+            pushEmoticon('0.o', 'wide_eye_left');
+            break;
+        case ':\\':
+        case '=\\':
+        case ':/':
+        case '=/':
+            pushEmoticon(':\\', 'unsure');
+            break;
+        default:
+            words_out.push(words_in[i]);
+        }
+    }
+
+    return words_out.join(' ');
+}
+
+// Code based on http://anentropic.wordpress.com/2009/06/25/javascript-iso8601-parser-and-pretty-dates/#comment-154
+function parseISO8601(str) {
+    if (Date.prototype.toISOString) {
+        return new Date(str);
+    } else {
+        var parts = str.split('T'),
+            dateParts = parts[0].split('-'),
+            timeParts = parts[1].split('Z'),
+            timeSubParts = timeParts[0].split(':'),
+            timeSecParts = timeSubParts[2].split('.'),
+            timeHours = Number(timeSubParts[0]),
+            _date = new Date();
+
+        _date.setUTCFullYear(Number(dateParts[0]));
+        _date.setUTCDate(1);
+        _date.setUTCMonth(Number(dateParts[1])-1);
+        _date.setUTCDate(Number(dateParts[2]));
+        _date.setUTCHours(Number(timeHours));
+        _date.setUTCMinutes(Number(timeSubParts[1]));
+        _date.setUTCSeconds(Number(timeSecParts[0]));
+        if (timeSecParts[1]) {
+            _date.setUTCMilliseconds(Number(timeSecParts[1]));
+        }
+
+        return _date;
+    }
+}
+
+// Simplyfy the translation syntax
+function translateText(string_id, params) {
+    params = params || '';
+
+    return _kiwi.global.i18n.translate(string_id).fetch(params);
+}
+
+/**
+ * Simplyfy the text styling syntax
+ *
+ * Syntax:
+ *   %nick:     nickname
+ *   %channel:  channel
+ *   %ident:    ident
+ *   %host:     host
+ *   %realname: realname
+ *   %text:     translated text
+ *   %C[digit]: color
+ *   %B:        bold
+ *   %I:        italic
+ *   %U:        underline
+ *   %O:        cancel styles
+ **/
+function styleText(string_id, params) {
+    var style, text;
+
+    //style = formatToIrcMsg(_kiwi.app.text_theme[string_id]);
+    style = _kiwi.app.text_theme[string_id];
+    style = formatToIrcMsg(style);
+
+    // Expand a member mask into its individual parts (nick, ident, hostname)
+    if (params.member) {
+        params.nick = params.member.nick || '';
+        params.ident = params.member.ident || '';
+        params.host = params.member.hostname || '';
+        params.prefix = params.member.prefix || '';
+    }
+
+    // Do the magic. Use the %shorthand syntax to produce output.
+    text = style.replace(/%([A-Z]{2,})/ig, function(match, key) {
+        if (typeof params[key] !== 'undefined')
+            return params[key];
+    });
+
+    return text;
+}
+
+
+
+
+})(window);
\ No newline at end of file
diff --git a/client/assets/kiwi.min.js~ b/client/assets/kiwi.min.js~
new file mode 100644 (file)
index 0000000..a61e5b5
--- /dev/null
@@ -0,0 +1,5 @@
+!function(e,t){function n(){this._listeners={},this._parent=null,this._children=[]}function i(e){var t,n,i="0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz",s="";for(t=0;e>t;t++)n=Math.floor(Math.random()*i.length),s+=i.substring(n,n+1);return s}function s(e){var t,n,i,s,a,o;return t=Math.floor(e/3600),s=e%3600,n=Math.floor(s/60),a=s%60,i=Math.ceil(a),o={h:t,m:n,s:i}}function a(){this.recursive_depth=3,this.aliases={},this.vars={version:1};var e=0;this.processInput=function(e){var t,n=e||[],i=this.aliases[n[0]],s="",a=[];if(!i)return e;i=i.split(" "),t=i.length;for(var o=0;t>o;o++)if(s=i[o],"$"===s[0])if(isNaN(s[1]))"undefined"==typeof this.vars[s.substr(1)]||a.push(this.vars[s.substr(1)]);else{var c=s.match(/\$(\d+)(\+)?(\d+)?/);if(!c||!n[c[1]])continue;"+"===c[2]&&c[3]?a=a.concat(n.slice(parseInt(c[1],10),parseInt(c[1],10)+parseInt(c[3],10))):"+"===c[2]?a=a.concat(n.slice(parseInt(c[1],10))):a.push(n[parseInt(c[1],10)])}else a.push(s);return a},this.process=function(t){t=t||"";var n=t.split(" ");return e++,e>=this.recursive_depth?(e--,t):(this.aliases[n[0]]&&(n=this.processInput(n),this.aliases[n[0]]&&(n=this.process(n.join(" ")).split(" "))),e--,n.join(" "))}}function o(e,t,n){function i(e,t,n){var i;return 0>n?n+=1:n>1&&(n-=1),i=1>6*n?e+(t-e)*n*6:1>2*n?t:2>3*n?e+(t-e)*(2/3-n)*6:e,255*i}var s,a,o,c,l,r;return t/=100,n/=100,0==t?c=l=r=255*n:(a=.5>=n?n*(t+1):n+t-n*t,s=2*n-a,o=e/360,c=i(s,a,o+1/3),l=i(s,a,o),r=i(s,a,o-1/3)),[c,l,r]}function c(e){e=e.replace(/%C(\d)/g,function(e,t){return String.fromCharCode(3)+t.toString()});var t={B:"\ 2",I:"\1d",U:"\1f",O:"\ f"};return e=e.replace(/%([BIUO])/g,function(e,n){return"undefined"!=typeof t[n.toUpperCase()]?t[n.toUpperCase()]:void 0})}function l(e){"use strict";var t,n="",i="",s={bold:!1,italic:!1,underline:!1,colour:!1},a=function(){var e,t="";return s.bold||s.italic||s.underline||s.colour?(t+=s.bold?"font-weight: bold; ":"",t+=s.italic?"font-style: italic; ":"",t+=s.underline?"text-decoration: underline; ":"",s.colour&&(e=s.colour.split(","),t+="color: "+e[0]+(e[1]?"; background-color: "+e[1]+";":"")),'<span class="format_span" style="'+t+'">'):""},o=function(e){var t=/^\x03(([0-9][0-9]?)(,([0-9][0-9]?))?)/;return t.exec(e)},c=function(e){switch(parseInt(e,10)){case 0:return"#FFFFFF";case 1:return"#000000";case 2:return"#000080";case 3:return"#008000";case 4:return"#FF0000";case 5:return"#800040";case 6:return"#800080";case 7:return"#FF8040";case 8:return"#FFFF00";case 9:return"#80FF00";case 10:return"#008080";case 11:return"#00FFFF";case 12:return"#0000FF";case 13:return"#FF55FF";case 14:return"#808080";case 15:return"#C0C0C0";default:return null}},l=0,r=[];for(l=0;l<e.length;l++)switch(e[l]){case"\ 2":(s.bold||s.italic||s.underline||s.colour)&&(n+=i+"</span>"),s.bold=!s.bold,i=a();break;case"\1d":(s.bold||s.italic||s.underline||s.colour)&&(n+=i+"</span>"),s.italic=!s.italic,i=a();break;case"\1f":(s.bold||s.italic||s.underline||s.colour)&&(n+=i+"</span>"),s.underline=!s.underline,i=a();break;case"\ 3":(s.bold||s.italic||s.underline||s.colour)&&(n+=i+"</span>"),t=o(e.substr(l,6)),t?(l+=t[1].length,r[0]=c(t[2]),t[4]&&(r[1]=c(t[4])),s.colour=r.join(",")):s.colour=!1,i=a();break;case"\ f":(s.bold||s.italic||s.underline||s.colour)&&(n+=i+"</span>"),s.bold=s.italic=s.underline=s.colour=!1;break;default:s.bold||s.italic||s.underline||s.colour?i+=e[l]:n+=e[l]}return(s.bold||s.italic||s.underline||s.colour)&&(n+=i+"</span>"),n}function r(e){return e.replace(/[\[\\\^\$\.\|\?\*\+\(\)]/g,"\\$&")}function h(e){var t,n=e.split(" "),i=[],s=function(e,t){i.push('<i class="emoticon '+t+'">'+e+"</i>")};for(t=0;t<n.length;t++)switch(n[t]){case":)":s(":)","smile");break;case":(":s(":(","sad");break;case":3":s(":3","lion");break;case";3":s(";3","winky_lion");break;case":s":case":S":s(":s","confused");break;case";(":case";_;":s(";(","cry");break;case";)":s(";)","wink");break;case";D":s(";D","wink_happy");break;case":P":case":p":s(":P","tongue");break;case"xP":s("xP","cringe_tongue");break;case":o":case":O":case":0":s(":o","shocked");break;case":D":s(":D","happy");break;case"^^":case"^.^":s("^^,","eyebrows");break;case"&lt;3":s("<3","heart");break;case"&gt;_&lt;":case"&gt;.&lt;":s(">_<","doh");break;case"XD":case"xD":s("xD","big_grin");break;case"o.0":case"o.O":s("o.0","wide_eye_right");break;case"0.o":case"O.o":s("0.o","wide_eye_left");break;case":\\":case"=\\":case":/":case"=/":s(":\\","unsure");break;default:i.push(n[t])}return i.join(" ")}function p(e){if(Date.prototype.toISOString)return new Date(e);var t=e.split("T"),n=t[0].split("-"),i=t[1].split("Z"),s=i[0].split(":"),a=s[2].split("."),o=Number(s[0]),c=new Date;return c.setUTCFullYear(Number(n[0])),c.setUTCDate(1),c.setUTCMonth(Number(n[1])-1),c.setUTCDate(Number(n[2])),c.setUTCHours(Number(o)),c.setUTCMinutes(Number(s[1])),c.setUTCSeconds(Number(a[0])),a[1]&&c.setUTCMilliseconds(Number(a[1])),c}function d(e,t){return t=t||"",m.global.i18n.translate(e).fetch(t)}function u(e,t){var n,i;return n=m.app.text_theme[e],n=c(n),t.member&&(t.nick=t.member.nick||"",t.ident=t.member.ident||"",t.host=t.member.hostname||"",t.prefix=t.member.prefix||""),i=n.replace(/%([A-Z]{2,})/gi,function(e,n){return"undefined"!=typeof t[n]?t[n]:void 0})}var m={};if(m.misc={},m.model={},m.view={},m.applets={},m.utils={},m.global={build_version:"",settings:t,plugins:t,events:t,rpc:t,utils:{},initUtils:function(){this.utils.randomString=i,this.utils.secondsToTime=s,this.utils.parseISO8601=p,this.utils.escapeRegex=r,this.utils.formatIRCMsg=l,this.utils.styleText=u,this.utils.hsl2rgb=o,this.utils.notifications=m.utils.notifications,this.utils.formatDate=m.utils.formatDate},addMediaMessageType:function(e,t){m.view.MediaMessage.addType(e,t)},components:{EventComponent:function(e,t){function n(e,n){"all"==t||(n=e.event_data,e=e.event_name),this.trigger(e,n)}t=t||"all",_.extend(this,Backbone.Events),this._source=e,e.on(t,n,this),this.dispose=function(){e.off(t,n),this.off(),delete this.event_source}},Network:function(e){var n;n="undefined"!=typeof e?"connection:"+e.toString():"connection";var i=function(){var n="undefined"==typeof e?m.app.connections.active_connection:m.app.connections.getByConnectionId(e);return n?n:t},s=new this.EventComponent(m.gateway,n),a={kiwi:"kiwi",raw:"raw",kick:"kick",topic:"topic",part:"part",join:"join",action:"action",ctcp:"ctcp",ctcpRequest:"ctcpRequest",ctcpResponse:"ctcpResponse",notice:"notice",msg:"privmsg",say:"privmsg",changeNick:"changeNick",channelInfo:"channelInfo",mode:"mode",quit:"quit"};return _.each(a,function(t,n){s[n]=function(){var n=t,i=Array.prototype.slice.call(arguments,0);return i.unshift(e),m.gateway[n].apply(m.gateway,i)}}),s.createQuery=function(e){var t;return(t=i())?t.createQuery(e):void 0},s.get=function(e){var n,s;return(n=i())?(s=["password"],s.indexOf(e)>-1?t:n.get(e)):void 0},s.set=function(){var e=i();if(e)return e.set.apply(e,arguments)},s},ControlInput:function(){var e=new this.EventComponent(m.app.controlbox),t={run:"processInput",addPluginIcon:"addPluginIcon"};return _.each(t,function(t,n){e[n]=function(){var e=t;return m.app.controlbox[e].apply(m.app.controlbox,arguments)}}),e.input=m.app.controlbox.$(".inp"),e}},init:function(e,t){var i,s,a=this;e=e||{},this.initUtils(),m.global.settings=m.model.DataStore.instance("kiwi.settings"),m.global.settings.load(),window.document.title=e.server_settings.client.window_title||"Kiwi IRC",i=new Promise(function(t){var n=m.global.settings.get("locale")||"magic";$.getJSON(e.base_path+"/assets/locales/"+n+".json",function(e){a.i18n=e?new Jed(e):new Jed,t()})}),s=new Promise(function(t){var n=e.server_settings.client.settings.text_theme||"default";$.getJSON(e.base_path+"/assets/text_themes/"+n+".json",function(n){e.text_theme=n,t()})}),Promise.all([i,s]).then(function(){m.app=new m.model.Application(e),m.app.initializeInterfaces(),m.global.events=new n,m.global.plugins=new m.model.PluginManager,t()}).then(null,function(e){console.error(e.stack)})},start:function(){m.app.showStartup()},registerStartupApplet:function(e){m.app.startup_applet_name=e},newIrcConnection:function(e,t){m.gateway.newConnection(e,t)},defaultServerSettings:function(){var e,t,n={nick:"",server:"",port:6667,ssl:!1,channel:"",channel_key:""};return m.app.server_settings.client&&(m.app.server_settings.client.nick&&(n.nick=m.app.server_settings.client.nick),m.app.server_settings.client.server&&(n.server=m.app.server_settings.client.server),m.app.server_settings.client.port&&(n.port=m.app.server_settings.client.port),m.app.server_settings.client.ssl&&(n.ssl=m.app.server_settings.client.ssl),m.app.server_settings.client.channel&&(n.channel=m.app.server_settings.client.channel),m.app.server_settings.client.channel_key&&(n.channel_key=m.app.server_settings.client.channel_key)),getQueryVariable("nick")&&(n.nick=getQueryVariable("nick")),window.location.hash&&(n.channel=window.location.hash),e=window.location.pathname.toString().replace(m.app.get("base_path"),"").split("/"),e.length>0&&(e.shift(),e.length>0&&e[0]&&(t=e[0].substr(0,7).toLowerCase(),"ircs%3a"===t||"irc%3a"===t.substr(0,6)?(e[0]=decodeURIComponent(e[0]),t=/^irc(s)?:(?:\/\/?)?([^:\/]+)(?::([0-9]+))?(?:(?:\/)([^\?]*)(?:(?:\?)(.*))?)?$/.exec(e[0]),t&&("undefined"!=typeof t[1]&&(n.ssl=!0,6667===n.port&&(n.port=6697)),n.server=t[2],"undefined"!=typeof t[3]&&(n.port=t[3]),"undefined"!=typeof t[4]&&(n.channel="#"+t[4],"undefined"!=typeof t[5]&&(n.channel_key=t[5]))),e=[]):(e[0].search(/:/)>0?(n.port=e[0].substring(e[0].search(/:/)+1),n.server=e[0].substring(0,e[0].search(/:/)),"+"===n.port[0]?(n.port=parseInt(n.port.substring(1),10),n.ssl=!0):n.ssl=!1):n.server=e[0],e.shift())),e.length>0&&e[0]&&(n.channel="#"+e[0],e.shift())),m.app.server_settings&&m.app.server_settings.connection&&(m.app.server_settings.connection.server&&(n.server=m.app.server_settings.connection.server),m.app.server_settings.connection.port&&(n.port=m.app.server_settings.connection.port),m.app.server_settings.connection.ssl&&(n.ssl=m.app.server_settings.connection.ssl),m.app.server_settings.connection.channel&&(n.channel=m.app.server_settings.connection.channel),m.app.server_settings.connection.channel_key&&(n.channel_key=m.app.server_settings.connection.channel_key),m.app.server_settings.connection.nick&&(n.nick=m.app.server_settings.connection.nick)),n.nick=n.nick.replace("?",Math.floor(1e5*Math.random()).toString()),getQueryVariable("encoding")&&(n.encoding=getQueryVariable("encoding")),n}},"undefined"!=typeof e)e.kiwi=m.global;else var g=m.global;!function(){m.model.Application=Backbone.Model.extend({view:null,message:null,initialize:function(e){this.app_options=e,e.container&&this.set("container",e.container),this.set("base_path",e.base_path?e.base_path:""),this.set("settings_path",e.settings_path?e.settings_path:this.get("base_path")+"/assets/settings.json"),this.server_settings=e.server_settings||{},this.translations=e.translations||{},this.themes=e.themes||[],this.text_theme=e.text_theme||{},this.startup_applet_name=e.startup||"kiwi_startup",this.server_settings&&this.server_settings.client&&this.server_settings.client.settings&&this.applyDefaultClientSettings(this.server_settings.client.settings)},initializeInterfaces:function(){var e=this.app_options.kiwi_server||this.detectKiwiServer();m.gateway=new m.model.Gateway({kiwi_server:e}),this.bindGatewayCommands(m.gateway),this.initializeClient(),this.initializeGlobals(),this.view.barsHide(!0)},detectKiwiServer:function(){return"file:"===window.location.protocol?"http://localhost:7778":window.location.protocol+"//"+window.location.host},showStartup:function(){this.startup_applet=m.model.Applet.load(this.startup_applet_name,{no_tab:!0}),this.startup_applet.tab=this.view.$(".console"),this.startup_applet.view.show(),m.global.events.emit("loaded")},initializeClient:function(){this.view=new m.view.Application({model:this,el:this.get("container")}),this.connections=new m.model.NetworkPanelList,this.connections.on("remove",_.bind(function(){0===this.connections.length&&this.view.barsHide()},this)),this.applet_panels=new m.model.PanelList,this.applet_panels.view.$el.addClass("panellist applets"),this.view.$el.find(".tabs").append(this.applet_panels.view.$el),this.controlbox=new m.view.ControlBox({el:$("#kiwi .controlbox")[0]}).render(),this.client_ui_commands=new m.misc.ClientUiCommands(this,this.controlbox),this.rightbar=new m.view.RightBar({el:this.view.$(".right_bar")[0]}),this.topicbar=new m.view.TopicBar({el:this.view.$el.find(".topic")[0]}),new m.view.AppToolbar({el:m.app.view.$el.find(".toolbar .app_tools")[0]}),new m.view.ChannelTools({el:m.app.view.$el.find(".channel_tools")[0]}),this.message=new m.view.StatusMessage({el:this.view.$el.find(".status_message")[0]}),this.resize_handle=new m.view.ResizeHandler({el:this.view.$el.find(".memberlists_resize_handle")[0]}),this.view.doLayout()},initializeGlobals:function(){m.global.connections=this.connections,m.global.panels=this.panels,m.global.panels.applets=this.applet_panels,m.global.components.Applet=m.model.Applet,m.global.components.Panel=m.model.Panel,m.global.components.MenuBox=m.view.MenuBox,m.global.components.DataStore=m.model.DataStore,m.global.components.Notification=m.view.Notification,m.global.components.Events=function(){return g.events.createProxy()}},applyDefaultClientSettings:function(e){_.each(e,function(e,t){"undefined"==typeof m.global.settings.get(t)&&m.global.settings.set(t,e)})},panels:function(){var e,t=function(t){var n,i=m.app;switch(t=t||"connections"){case"connections":n=i.connections.panels();break;case"applets":n=i.applet_panels.models}return n.active=e,n.server=i.connections.active_connection?i.connections.active_connection.panels.server:null,n};return _.extend(t,Backbone.Events),t.bind("active",function(t){var n=e;e=t,m.global.events.emit("panel:active",{previous:n,active:e})}),t}(),bindGatewayCommands:function(e){var t=this;e.on("connection:connect",function(){t.view.barsShow()}),function(){var n=0;e.on("disconnect",function(){t.view.$el.removeClass("connected"),n=1}),e.on("reconnecting",function(e){var t=d("client_models_application_reconnect_in_x_seconds",[e.delay/1e3])+"...";m.app.connections.forEach(function(e){e.panels.server.addMsg("",u("quit",{text:t}),"action quit")})}),e.on("kiwi:connected",function(){var e;t.view.$el.addClass("connected"),m.global.rpc=m.gateway.rpc,m.global.events.emit("connected"),1===n&&(n=0,e=d("client_models_application_reconnect_successfully")+" :)",t.message.text(e,{timeout:5e3}),m.app.connections.forEach(function(t){t.reconnect(),t.panels.server.addMsg("",u("rejoin",{text:e}),"action join"),t.panels.forEach(function(t){t.isChannel()&&t.addMsg("",u("rejoin",{text:e}),"action join")})}))})}(),e.on("kiwi:reconfig",function(){$.getJSON(t.get("settings_path"),function(e){t.server_settings=e.server_settings||{},t.translations=e.translations||{}})}),e.on("kiwi:jumpserver",function(e){var n;if("undefined"!=typeof e.kiwi_server&&(n=e.kiwi_server,"/"===n[n.length-1]&&(n=n.substring(0,n.length-1)),e.force)){var i=60*Math.random()+300;i=1;var s=m.global.i18n.translate("client_models_application_jumpserver_prepare").fetch();t.message.text(s,{timeout:1e4}),setTimeout(function(){var e=m.global.i18n.translate("client_models_application_jumpserver_reconnect").fetch();t.message.text(e,{timeout:8e3}),setTimeout(function(){m.gateway.set("kiwi_server",n),m.gateway.reconnect(function(){t.connections.forEach(function(e){e.reconnect()})})},5e3)},1e3*i)}})}})}(),m.model.Gateway=Backbone.Model.extend({initialize:function(){this.socket=this.get("socket"),this.disconnect_requested=!1},reconnect:function(e){this.disconnect_requested=!0,this.socket.close(),this.socket=null,this.connect(e)},connect:function(e){var t=this;this.connect_callback=e,this.socket=new EngineioTools.ReconnectingSocket(this.get("kiwi_server"),{transports:m.app.server_settings.transports||["polling","websocket"],path:m.app.get("base_path")+"/transport",reconnect_max_attempts:5,reconnect_delay:2e3}),this.rpc&&rpc.dispose(),this.rpc=new EngineioTools.Rpc(this.socket),this.socket.on("connect_failed",function(e){this.socket.disconnect(),this.trigger("connect_fail",{reason:e})}),this.socket.on("error",function(e){console.log("_kiwi.gateway.socket.on('error')",{reason:e}),t.connect_callback&&(t.connect_callback(e),delete t.connect_callback),t.trigger("connect_fail",{reason:e})}),this.socket.on("connecting",function(){console.log("_kiwi.gateway.socket.on('connecting')"),t.trigger("connecting")}),this.socket.on("open",function(){t.disconnect_requested=!1;var e=function(){t.rpc&&(t.rpc("kiwi.heartbeat"),t._heartbeat_tmr=setTimeout(e,6e4))};e(),console.log("_kiwi.gateway.socket.on('open')")}),this.rpc.on("too_many_connections",function(){t.trigger("connect_fail",{reason:"too_many_connections"})}),this.rpc.on("irc",function(e,n){t.parse(n.command,n.data)}),this.rpc.on("kiwi",function(e,n){t.parseKiwi(n.command,n.data)}),this.socket.on("close",function(){t.trigger("disconnect",{}),console.log("_kiwi.gateway.socket.on('close')")}),this.socket.on("reconnecting",function(e){console.log("_kiwi.gateway.socket.on('reconnecting')"),t.trigger("reconnecting",{delay:e.delay,attempts:e.attempts})}),this.socket.on("reconnecting_failed",function(){console.log("_kiwi.gateway.socket.on('reconnect_failed')")})},newConnection:function(e,t){var n=this;return this.isConnected()?void this.makeIrcConnection(e,function(n,i){var s;if(n)console.log("_kiwi.gateway.socket.on('error')",{reason:n}),t&&t(n);else{if(!m.app.connections.getByConnectionId(i)){var a={connection_id:i,nick:e.nick,address:e.host,port:e.port,ssl:e.ssl,password:e.password};s=new m.model.Network(a),m.app.connections.add(s)}console.log("_kiwi.gateway.socket.on('connect')",s),t&&t(n,s)}}):void this.connect(function(i){return i?void t(i):void n.newConnection(e,t)})},makeIrcConnection:function(e,t){var n={nick:e.nick,hostname:e.host,port:e.port,ssl:e.ssl,password:e.password};e.options=e.options||{},e.options.encoding&&(n.encoding=e.options.encoding),this.rpc("kiwi.connect_irc",n,function(e,n){e?t&&t(e):t&&t(e,n)})},isConnected:function(){return this.socket},parseKiwi:function(e,t){var n;switch(e){case"connected":n={build_version:m.global.build_version},this.rpc("kiwi.client_info",n),this.connect_callback&&this.connect_callback(),delete this.connect_callback}this.trigger("kiwi:"+e,t),this.trigger("kiwi",t)},parse:function(e,t){var n="";"undefined"!=typeof t.connection_id&&(n="connection:"+t.connection_id.toString(),this.trigger(n,{event_name:e,event_data:t}),"message"==e&&t.type&&this.trigger("connection "+n,{event_name:"message:"+t.type,event_data:t}),"channel"==e&&t.type&&this.trigger("connection "+n,{event_name:"channel:"+t.type,event_data:t})),this.trigger("connection",{event_name:e,event_data:t}),this.trigger("connection:"+e,t)},rpcCall:function(){var e=Array.prototype.slice.call(arguments,0);return("undefined"==typeof e[1]||null===e[1])&&(e[1]=m.app.connections.active_connection.get("connection_id")),this.rpc.apply(this.rpc,e)},privmsg:function(e,t,n,i){var s={target:t,msg:n};this.rpcCall("irc.privmsg",e,s,i)},notice:function(e,t,n,i){var s={target:t,msg:n};this.rpcCall("irc.notice",e,s,i)},ctcp:function(e,t,n,i,s,a){var o={is_request:t,type:n,target:i,params:s};this.rpcCall("irc.ctcp",e,o,a)},ctcpRequest:function(e,t,n,i,s){this.ctcp(e,!0,t,n,i,s)},ctcpResponse:function(e,t,n,i,s){this.ctcp(e,!1,t,n,i,s)},action:function(e,t,n,i){this.ctcp(e,!0,"ACTION",t,n,i)},join:function(e,t,n,i){var s={channel:t,key:n};this.rpcCall("irc.join",e,s,i)},channelInfo:function(e,t,n){var i={channel:t};this.rpcCall("irc.channel_info",e,i,n)},part:function(e,n,i,s){"use strict";"function"==typeof arguments[2]&&(s=arguments[2],i=t);var a={channel:n,message:i};this.rpcCall("irc.part",e,a,s)},topic:function(e,t,n,i){var s={channel:t,topic:n};this.rpcCall("irc.topic",e,s,i)},kick:function(e,t,n,i,s){var a={channel:t,nick:n,reason:i};this.rpcCall("irc.kick",e,a,s)},quit:function(e,t,n){t=t||"";var i={message:t};this.rpcCall("irc.quit",e,i,n)},raw:function(e,t,n){var i={data:t};this.rpcCall("irc.raw",e,i,n)},changeNick:function(e,t,n){var i={nick:t};this.rpcCall("irc.nick",e,i,n)},mode:function(e,t,n,i){var s={data:"MODE "+t+" "+n};this.rpcCall("irc.raw",e,s,i)},setEncoding:function(e,t,n){var i={encoding:t};this.rpcCall("irc.encoding",e,i,n)}}),function(){function e(){this.set("connected",!1),$.each(this.panels.models,function(e,t){t.isApplet()||t.addMsg("",u("network_disconnected",{text:d("client_models_network_disconnected",[])}),"action quit")})}function n(e){var t;this.set("nick",e.nick),this.set("connected",!0),this.rejoinAllChannels(),this.auto_join&&this.auto_join.channel&&(t=this.createAndJoinChannels(this.auto_join.channel+" "+(this.auto_join.key||"")),t&&t[t.length-1].view.show(),delete this.auto_join)}function i(e){var t=this;$.each(e.options,function(e,n){switch(e){case"CHANTYPES":t.set("channel_prefix",n.join(""));break;case"NETWORK":t.set("name",n);break;case"PREFIX":t.set("user_prefixes",n)}}),this.set("cap",e.cap)}function a(e){this.panels.server.addMsg(this.get("name"),u("motd",{text:e.msg}),"motd")}function o(e){var t,n,i;t=this.panels.getByName(e.channel),t||(t=new m.model.Channel({name:e.channel,network:this}),this.panels.add(t)),n=t.get("members"),n&&(n.getByNick(e.nick)||(i=new m.model.Member({nick:e.nick,ident:e.ident,hostname:e.hostname,user_prefixes:this.get("user_prefixes")}),m.global.events.emit("channel:join",{channel:e.channel,user:i,network:this.gateway}).then(function(){n.add(i,{kiwi:e})})))}function c(e){var t,n,i,s={};if(s.type="part",s.message=e.message||"",s.time=e.time,t=this.panels.getByName(e.channel)){if(e.nick===this.get("nick"))return void t.close();n=t.get("members"),n&&(i=n.getByNick(e.nick),i&&m.global.events.emit("channel:leave",{channel:e.channel,user:i,type:"part",message:s.message,network:this.gateway}).then(function(){n.remove(i,{kiwi:s})}))}}function l(e){var t,n={};n.type="quit",n.message=e.message||"",n.time=e.time,$.each(this.panels.models,function(i,s){s.isQuery()&&s.get("name").toLowerCase()===e.nick.toLowerCase()&&s.addMsg(" ",u("channel_quit",{nick:e.nick,text:d("client_models_channel_quit",[n.message])}),"action quit",{time:n.time}),s.isChannel()&&(t=s.get("members").getByNick(e.nick),t&&m.global.events.emit("channel:leave",{channel:s.get("name"),user:t,type:"quit",message:n.message,network:this.gateway}).then(function(){s.get("members").remove(t,{kiwi:n})}))})}function r(e){var t,n,i,s={};s.type="kick",s.by=e.nick,s.message=e.message||"",s.current_user_kicked=e.kicked==this.get("nick"),s.current_user_initiated=e.nick==this.get("nick"),s.time=e.time,t=this.panels.getByName(e.channel),t&&(n=t.get("members"),n&&(i=n.getByNick(e.kicked),i&&m.global.events.emit("channel:leave",{channel:e.channel,user:i,type:"kick",message:s.message,network:this.gateway}).then(function(){n.remove(i,{kiwi:s}),s.current_user_kicked&&n.reset([])})))}function h(e){m.global.events.emit("message:new",{network:this.gateway,message:e}).then(_.bind(function(){var t,n=(e.target||"").toLowerCase()==this.get("nick").toLowerCase();if(!this.isNickIgnored(e.nick))switch("notice"==e.type?(e.from_server?t=this.panels.server:(t=this.panels.getByName(e.target)||this.panels.getByName(e.nick),e.nick&&"chanserv"==e.nick.toLowerCase()&&"["==e.msg.charAt(0)&&(channel_name=/\[([^ \]]+)\]/gi.exec(e.msg),channel_name&&channel_name[1]&&(channel_name=channel_name[1],t=this.panels.getByName(channel_name)))),t||(t=this.panels.server)):n?(t=this.panels.getByName(e.nick),t||(t=new m.model.Query({name:e.nick,network:this}),this.panels.add(t))):(t=this.panels.getByName(e.target),t||(t=this.panels.server)),e.type){case"message":t.addMsg(e.nick,u("privmsg",{text:e.msg}),"privmsg",{time:e.time});break;case"action":t.addMsg("",u("action",{nick:e.nick,text:e.msg}),"action",{time:e.time});break;case"notice":t.addMsg("["+(e.nick||"")+"]",u("notice",{text:e.msg}),"notice",{time:e.time}),active_panel=m.app.panels().active,e.from_server||t!==this.panels.server||active_panel===this.panels.server||active_panel.get("network")===this&&(active_panel.isChannel()||active_panel.isQuery())&&active_panel.addMsg("["+(e.nick||"")+"]",u("notice",{text:e.msg}),"notice",{time:e.time})}},this))}function p(e){var t;$.each(this.panels.models,function(n,i){i.get("name")==e.nick&&i.set("name",e.newnick),i.isChannel()&&(t=i.get("members").getByNick(e.nick),t&&(t.set("nick",e.newnick),i.addMsg("",u("nick_changed",{nick:e.nick,text:d("client_models_network_nickname_changed",[e.newnick]),channel:name}),"action nick",{time:e.time})))})}function g(e){this.isNickIgnored(e.nick)||("TIME"===e.msg.toUpperCase()?this.gateway.ctcpResponse(e.type,e.nick,(new Date).toString()):"PING"===e.type.toUpperCase()&&this.gateway.ctcpResponse(e.type,e.nick,e.msg.substr(5)))}function f(e){this.isNickIgnored(e.nick)||this.panels.server.addMsg("["+e.nick+"]",u("ctcp",{text:e.msg}),"ctcp",{time:e.time})}function v(e){var t;t=this.panels.getByName(e.channel),t&&(t.set("topic",e.topic),t.get("name")===this.panels.active.get("name")&&m.app.topicbar.setCurrentTopic(e.topic))}function w(e){var t,n;t=this.panels.getByName(e.channel),t&&(n=new Date(1e3*e.when),t.set("topic_set_by",{nick:e.nick,when:n}))}function k(e){var t=this.panels.getByName(e.channel);t&&(e.url?t.set("info_url",e.url):e.modes&&t.set("info_modes",e.modes))}function b(e){var t=this,n=this.panels.getByName(e.channel);n&&(n.temp_userlist=n.temp_userlist||[],_.each(e.users,function(e){var i=new m.model.Member({nick:e.nick,modes:e.modes,user_prefixes:t.get("user_prefixes")});n.temp_userlist.push(i)}))}function y(e){var t;t=this.panels.getByName(e.channel),t&&(t.get("members").reset(t.temp_userlist||[]),delete t.temp_userlist)}function C(e){var t=this.panels.getByName(e.channel);t&&t.set("banlist",e.bans||[])}function x(e){function t(t,n){var i,s={};return t||(t=e.modes,n=e.target),_.each(t,function(e){var t=e.param||n||"";s[t]||(s[t]={"+":"","-":""}),s[t][e.mode[0]]+=e.mode.substr(1)}),i=[],_.each(s,function(e,t){var n="";e["+"]&&(n+="+"+e["+"]),e["-"]&&(n+="-"+e["-"]),i.push(n+" "+t)}),i=i.join(", ")}var n,i,s,a,o,c,l=!1;if(n=this.panels.getByName(e.target)){for(s=this.get("user_prefixes"),c=function(t){return e.modes[i].mode[1]===t.mode},i=0;i<e.modes.length;i++){if(_.any(s,c)){if(a||(a=n.get("members")),o=a.getByNick(e.modes[i].param),!o)return void console.log("MODE command recieved for unknown member %s on channel %s",e.modes[i].param,e.target);"+"===e.modes[i].mode[0]?o.addMode(e.modes[i].mode[1]):"-"===e.modes[i].mode[0]&&o.removeMode(e.modes[i].mode[1]),a.sort()}"b"==e.modes[i].mode[1]&&(l=!0)}n.addMsg("",u("mode",{nick:e.nick,text:d("client_models_network_mode",[t()]),channel:e.target}),"action mode",{time:e.time}),l&&this.gateway.raw("MODE "+n.get("name")+" +b")}else e.target.toLowerCase()===this.get("nick").toLowerCase()?this.panels.server.addMsg("",u("selfmode",{nick:e.nick,text:d("client_models_network_mode",[t()]),channel:e.target}),"action mode"):console.log("MODE command recieved for unknown target %s: ",e.target,e)}function M(e){var t,n,i="";e.end||("undefined"!=typeof e.idle&&(i=s(parseInt(e.idle,10)),i=i.h.toString().lpad(2,"0")+":"+i.m.toString().lpad(2,"0")+":"+i.s.toString().lpad(2,"0")),n=m.app.panels().active,e.ident?n.addMsg(e.nick,u("whois_ident",{nick:e.nick,ident:e.ident,host:e.hostname,text:e.msg}),"whois"):e.chans?n.addMsg(e.nick,u("whois_channels",{nick:e.nick,text:d("client_models_network_channels",[e.chans])}),"whois"):e.irc_server?n.addMsg(e.nick,u("whois_server",{nick:e.nick,text:d("client_models_network_server",[e.irc_server,e.server_info])}),"whois"):e.msg?n.addMsg(e.nick,u("whois",{text:e.msg}),"whois"):e.logon?(t=new Date,t.setTime(1e3*e.logon),t=m.utils.formatDate(t),n.addMsg(e.nick,u("whois_idle_and_signon",{nick:e.nick,text:d("client_models_network_idle_and_signon",[i,t])}),"whois")):e.away_reason?n.addMsg(e.nick,u("whois_away",{nick:e.nick,text:d("client_models_network_away",[e.away_reason])}),"whois"):n.addMsg(e.nick,u("whois_idle",{nick:e.nick,text:d("client_models_network_idle",[i])}),"whois"))}function S(e){var t;e.end||(t=m.app.panels().active,e.hostname?t.addMsg(e.nick,u("who",{nick:e.nick,ident:e.ident,host:e.hostname,realname:e.real_name,text:e.msg}),"whois"):t.addMsg(e.nick,u("whois_notfound",{nick:e.nick,text:d("client_models_network_nickname_notfound",[])}),"whois"))}function T(e){$.each(this.panels.models,function(t,n){n.isChannel()&&(member=n.get("members").getByNick(e.nick),member&&member.set("away",!!e.reason))})}function N(){var e=m.model.Applet.loadOnce("kiwi_chanlist");e.view.show()}function B(e){var n,i;switch(e.channel===t||(n=this.panels.getByName(e.channel))||(n=this.panels.server),e.error){case"banned_from_channel":n.addMsg(" ",u("channel_banned",{nick:e.nick,text:d("client_models_network_banned",[e.channel,e.reason]),channel:e.channel}),"status"),m.app.message.text(m.global.i18n.translate("client_models_network_banned").fetch(e.channel,e.reason));break;case"bad_channel_key":n.addMsg(" ",u("channel_badkey",{nick:e.nick,text:d("client_models_network_channel_badkey",[e.channel]),channel:e.channel}),"status"),m.app.message.text(m.global.i18n.translate("client_models_network_channel_badkey").fetch(e.channel));break;case"invite_only_channel":n.addMsg(" ",u("channel_inviteonly",{nick:e.nick,text:d("client_models_network_channel_inviteonly",[e.nick,e.channel]),channel:e.channel}),"status"),m.app.message.text(e.channel+" "+m.global.i18n.translate("client_models_network_channel_inviteonly").fetch());break;case"user_on_channel":n.addMsg(" ",u("channel_alreadyin",{nick:e.nick,text:d("client_models_network_channel_alreadyin"),channel:e.channel}));break;case"channel_is_full":n.addMsg(" ",u("channel_limitreached",{nick:e.nick,text:d("client_models_network_channel_limitreached",[e.channel]),channel:e.channel}),"status"),m.app.message.text(e.channel+" "+m.global.i18n.translate("client_models_network_channel_limitreached").fetch(e.channel));break;case"chanop_privs_needed":n.addMsg(" ",u("chanop_privs_needed",{text:e.reason,channel:e.channel}),"status"),m.app.message.text(e.reason+" ("+e.channel+")");break;case"cannot_send_to_channel":n.addMsg(" ","== "+m.global.i18n.translate("Cannot send message to channel, you are not voiced").fetch(e.channel,e.reason),"status");break;case"no_such_nick":i=this.panels.getByName(e.nick),i?i.addMsg(" ",u("no_such_nick",{nick:e.nick,text:e.reason,channel:e.channel}),"status"):this.panels.server.addMsg(" ",u("no_such_nick",{nick:e.nick,text:e.reason,channel:e.channel}),"status");break;case"nickname_in_use":this.panels.server.addMsg(" ",u("nickname_alreadyinuse",{nick:e.nick,text:d("client_models_network_nickname_alreadyinuse",[e.nick]),channel:e.channel}),"status"),this.panels.server!==this.panels.active&&m.app.message.text(m.global.i18n.translate("client_models_network_nickname_alreadyinuse").fetch(e.nick)),"none"!==m.app.controlbox.$el.css("display")&&(new m.view.NickChangeBox).render();break;case"password_mismatch":this.panels.server.addMsg(" ",u("channel_badpassword",{nick:e.nick,text:d("client_models_network_badpassword",[]),channel:e.channel}),"status");break;case"error":e.reason&&this.panels.server.addMsg(" ",u("general_error",{text:e.reason}),"status")}}function D(e){var t=_.clone(e.params);t[0]&&t[0]==this.get("nick")&&t.shift(),this.panels.server.addMsg("",u("unknown_command",{text:"["+e.command+"] "+t.join(", ","")}))}function I(e){var t=m.app.panels().active;this.panels.server.addMsg("["+(e.nick||"")+"]",u("wallops",{text:e.msg}),"wallops",{time:e.time}),t!==this.panels.server&&(t.isChannel()||t.isQuery())&&t.get("network")===this&&t.addMsg("["+(e.nick||"")+"]",u("wallops",{text:e.msg}),"wallops",{time:e.time})}m.model.Network=Backbone.Model.extend({defaults:{connection_id:0,name:"Network",address:"",port:6667,ssl:!1,password:"",nick:"",channel_prefix:"#",user_prefixes:[{symbol:"~",mode:"q"},{symbol:"&",mode:"a"},{symbol:"@",mode:"o"},{symbol:"%",mode:"h"},{symbol:"+",mode:"v"}],ignore_list:[]},initialize:function(){"undefined"!=typeof this.get("connection_id")&&(this.gateway=m.global.components.Network(this.get("connection_id")),this.bindGatewayEvents()),this.panels=new m.model.PanelList([],this);
+var e=new m.model.Server({name:"Server",network:this});this.panels.add(e),this.panels.server=this.panels.active=e},reconnect:function(e){var t=this,n={nick:this.get("nick"),host:this.get("address"),port:this.get("port"),ssl:this.get("ssl"),password:this.get("password")};m.gateway.makeIrcConnection(n,function(n,i){n?(console.log("_kiwi.gateway.socket.on('error')",{reason:n}),e&&e(n)):(t.gateway.dispose(),t.set("connection_id",i),t.gateway=m.global.components.Network(t.get("connection_id")),t.bindGatewayEvents(),t.panels.forEach(function(e){e.set("connection_id",i)}),e&&e(n))})},bindGatewayEvents:function(){this.gateway.on("connect",n,this),this.gateway.on("disconnect",e,this),this.gateway.on("nick",function(e){e.nick===this.get("nick")&&this.set("nick",e.newnick)},this),this.gateway.on("options",i,this),this.gateway.on("motd",a,this),this.gateway.on("channel:join",o,this),this.gateway.on("channel:part",c,this),this.gateway.on("channel:kick",r,this),this.gateway.on("quit",l,this),this.gateway.on("message",h,this),this.gateway.on("nick",p,this),this.gateway.on("ctcp_request",g,this),this.gateway.on("ctcp_response",f,this),this.gateway.on("topic",v,this),this.gateway.on("topicsetby",w,this),this.gateway.on("userlist",b,this),this.gateway.on("userlist_end",y,this),this.gateway.on("banlist",C,this),this.gateway.on("mode",x,this),this.gateway.on("whois",M,this),this.gateway.on("whowas",S,this),this.gateway.on("away",T,this),this.gateway.on("list_start",N,this),this.gateway.on("irc_error",B,this),this.gateway.on("unknown_command",D,this),this.gateway.on("channel_info",k,this),this.gateway.on("wallops",I,this)},createAndJoinChannels:function(e){var t=this,n=[];return"string"==typeof e&&(e=e.split(",")),$.each(e,function(e,i){var s=i.trim().split(" "),a=s[0],o=s[1]||"";a=a.trim(),-1===t.get("channel_prefix").indexOf(a[0])&&(a="#"+a),channel=t.panels.getByName(a),channel||(channel=new m.model.Channel({name:a,network:t}),t.panels.add(channel)),n.push(channel),t.gateway.join(a,o)}),n},rejoinAllChannels:function(){var e=this;this.panels.forEach(function(t){t.isChannel()&&e.gateway.join(t.get("name"))})},isChannelName:function(e){var t=this.get("channel_prefix");return e&&e.length?t.indexOf(e[0])>-1:!1},isNickIgnored:function(e){var t,n,i,s=this.get("ignore_list");for(t=0;t<s.length;t++)if(n=s[t].replace(/([.+^$[\]\\(){}|-])/g,"\\$1").replace("*",".*").replace("?","."),i=new RegExp(n,"i"),i.test(e))return!0;return!1},createQuery:function(e){var t,n=this;return t=n.panels.getByName(e),t||(t=new m.model.Query({name:e}),n.panels.add(t)),t.view.show(),t}})}(),m.model.Member=Backbone.Model.extend({initialize:function(){var e,t;e=this.stripPrefix(this.get("nick")),t=this.get("modes"),t=t||[],this.sortModes(t),this.set({nick:e,modes:t,prefix:this.getPrefix(t)},{silent:!0}),this.updateOpStatus(),this.view=new m.view.Member({model:this})},sortModes:function(e){var t=this;return e.sort(function(e,n){var i,s,a,o=t.get("user_prefixes");for(a=0;a<o.length;a++)o[a].mode===e&&(i=a);for(a=0;a<o.length;a++)o[a].mode===n&&(s=a);return s>i?-1:i>s?1:0})},addMode:function(e){var t,n=e.split("");t=this.get("modes"),$.each(n,function(e,n){t.push(n)}),t=this.sortModes(t),this.set({prefix:this.getPrefix(t),modes:t}),this.updateOpStatus(),this.view.render()},removeMode:function(e){var t,n=e.split("");t=this.get("modes"),t=_.reject(t,function(e){return-1!==_.indexOf(n,e)}),this.set({prefix:this.getPrefix(t),modes:t}),this.updateOpStatus(),this.view.render()},getPrefix:function(e){var t="",n=this.get("user_prefixes");return"undefined"!=typeof e[0]&&(t=_.detect(n,function(t){return t.mode===e[0]}),t=t?t.symbol:""),t},stripPrefix:function(e){var t,n,i,s,a=e,o=this.get("user_prefixes");t=0;e:for(n=0;n<e.length;n++){for(s=e.charAt(n),i=0;i<o.length;i++)if(s===o[i].symbol){t++;continue e}break}return a.substr(t)},displayNick:function(e){var t=this.get("nick");return e&&this.get("ident")&&(t+=" ["+this.get("ident")+"@"+this.get("hostname")+"]"),t},getMaskParts:function(){return{nick:this.get("nick")||"",ident:this.get("ident")||"",hostname:this.get("hostname")||""}},updateOpStatus:function(){var e,t,n=this.get("user_prefixes"),i=this.get("modes");i.length>0?(e=_.indexOf(n,_.find(n,function(e){return"o"===e.mode})),t=_.indexOf(n,_.find(n,function(e){return e.mode===i[0]})),-1===t||t>e?this.set({is_op:!1},{silent:!0}):this.set({is_op:!0},{silent:!0})):this.set({is_op:!1},{silent:!0})}}),m.model.MemberList=Backbone.Collection.extend({model:m.model.Member,comparator:function(e,t){var n,i,s,a,o,c,l,r=this.channel.get("network").get("user_prefixes");if(i=e.get("modes"),s=t.get("modes"),i.length>0){if(0===s.length)return-1;for(a=o=-1,n=0;n<r.length;n++)r[n].mode===i[0]&&(a=n);for(n=0;n<r.length;n++)r[n].mode===s[0]&&(o=n);if(o>a)return-1;if(a>o)return 1}else if(s.length>0)return 1;return c=e.get("nick").toLocaleUpperCase(),l=t.get("nick").toLocaleUpperCase(),l>c?-1:c>l?1:0},initialize:function(){this.view=new m.view.MemberList({model:this}),this.initNickCache()},initNickCache:function(){var e=this;this.nick_cache=Object.create(null),this.on("reset",function(){this.nick_cache=Object.create(null),this.models.forEach(function(t){e.nick_cache[t.get("nick").toLowerCase()]=t})}),this.on("add",function(t){e.nick_cache[t.get("nick").toLowerCase()]=t}),this.on("remove",function(t){delete e.nick_cache[t.get("nick").toLowerCase()]}),this.on("change:nick",function(t){e.nick_cache[t.get("nick").toLowerCase()]=t,delete e.nick_cache[t.previous("nick").toLowerCase()]})},getByNick:function(e){return"string"==typeof e?this.nick_cache[e.toLowerCase()]:void 0}}),m.model.NewConnection=Backbone.Collection.extend({initialize:function(){this.view=new m.view.ServerSelect({model:this}),this.view.bind("server_connect",this.onMakeConnection,this)},populateDefaultServerSettings:function(){var e=m.global.defaultServerSettings();this.view.populateFields(e)},onMakeConnection:function(e){var t=this;this.connect_details=e,this.view.networkConnecting(),m.gateway.newConnection({nick:e.nick,host:e.server,port:e.port,ssl:e.ssl,password:e.password,options:e.options},function(e,n){t.onNewNetwork(e,n)})},onNewNetwork:function(e,t){e&&this.view.showError(e),t&&this.connect_details&&(t.auto_join={channel:this.connect_details.channel,key:this.connect_details.channel_key},this.trigger("new_network",t))}}),m.model.Panel=Backbone.Model.extend({initialize:function(){var e=this.get("name")||"";this.view=new m.view.Panel({model:this,name:e}),this.set({scrollback:[],name:e},{silent:!0}),m.global.events.emit("panel:created",{panel:this})},close:function(){m.app.panels.trigger("close",this),m.global.events.emit("panel:close",{panel:this}),this.view&&(this.view.unbind(),this.view.remove(),this.view=t,delete this.view);var e=this.get("members");e&&(e.reset([]),this.unset("members")),this.get("panel_list").remove(this),this.unbind(),this.destroy()},isChannel:function(){return!1},isQuery:function(){return!1},isApplet:function(){return!1},isServer:function(){return!1},isActive:function(){return m.app.panels().active===this}}),m.model.PanelList=Backbone.Collection.extend({model:m.model.Panel,comparator:function(e){return e.get("name")},initialize:function(e,t){t&&(this.network=t),this.view=new m.view.Tabs({model:this}),this.active=null,this.bind("active",function(e){this.active=e},this),this.bind("add",function(e){e.set("panel_list",this)})},getByCid:function(e){return"string"==typeof name?this.find(function(t){return e===t.cid}):void 0},getByName:function(e){return"string"==typeof e?this.find(function(t){return e.toLowerCase()===t.get("name").toLowerCase()}):void 0}}),m.model.NetworkPanelList=Backbone.Collection.extend({model:m.model.Network,initialize:function(){this.view=new m.view.NetworkTabs({model:this}),this.on("add",this.onNetworkAdd,this),this.on("remove",this.onNetworkRemove,this),this.active_connection=t,this.active_panel=t,this.active=t},getByConnectionId:function(e){return this.find(function(t){return t.get("connection_id")==e})},panels:function(){var e=[];return this.each(function(t){e=e.concat(t.panels.models)}),e},onNetworkAdd:function(e){e.panels.on("active",this.onPanelActive,this),1===this.models.length&&(this.active_connection=e,this.active_panel=e.panels.server,this.active=this.active_panel)},onNetworkRemove:function(e){e.panels.off("active",this.onPanelActive,this)},onPanelActive:function(e){var t=this.getByConnectionId(e.tab.data("connection_id"));this.trigger("active",e,t),this.active_connection=t,this.active_panel=e,this.active=e}}),m.model.Channel=m.model.Panel.extend({initialize:function(){var e,t=this.get("name")||"";this.set({members:new m.model.MemberList,name:t,scrollback:[],topic:""},{silent:!0}),this.view=new m.view.Channel({model:this,name:t}),e=this.get("members"),e.channel=this,e.bind("add",function(e,n,i){var s=m.global.settings.get("show_joins_parts");s!==!1&&this.addMsg(" ",u("channel_join",{member:e.getMaskParts(),text:d("client_models_channel_join"),channel:t}),"action join",{time:i.kiwi.time})},this),e.bind("remove",function(e,n,i){var s=m.global.settings.get("show_joins_parts"),a=i.kiwi.message?"("+i.kiwi.message+")":"";"quit"===i.kiwi.type&&s?this.addMsg(" ",u("channel_quit",{member:e.getMaskParts(),text:d("client_models_channel_quit",[a]),channel:t}),"action quit",{time:i.kiwi.time}):"kick"===i.kiwi.type?i.kiwi.current_user_kicked?this.addMsg(" ",u("channel_selfkick",{text:d("client_models_channel_selfkick",[i.kiwi.by,a]),channel:t}),"action kick",{time:i.kiwi.time}):(s||i.kiwi.current_user_initiated)&&this.addMsg(" ",u("channel_kicked",{member:e.getMaskParts(),text:d("client_models_channel_kicked",[i.kiwi.by,a]),channel:t}),"action kick",{time:i.kiwi.time}):s&&this.addMsg(" ",u("channel_part",{member:e.getMaskParts(),text:d("client_models_channel_part",[a]),channel:t}),"action part",{time:i.kiwi.time})},this),m.global.events.emit("panel:created",{panel:this})},addMsg:function(e,t,n,i){var s,a,o,c,l=parseInt(m.global.settings.get("scrollback"),10)||250;i=i||{},i.time="number"==typeof i.time?new Date(i.time):new Date,i&&"undefined"!=typeof i.style||(i.style=""),s={msg:t,date:i.date,time:i.time,nick:e,chan:this.get("name"),type:n,style:i.style},o=this.get("members"),o&&(c=o.getByNick(s.nick),c&&(s.nick_prefix=c.get("prefix"))),"string"!=typeof s.type&&(s.type=""),"string"!=typeof s.msg&&(s.msg=""),a=this.get("scrollback"),a&&(a.push(s),a.length>l&&(a=_.last(a,l)),this.set({scrollback:a},{silent:!0})),this.trigger("msg",s)},clearMessages:function(){this.set({scrollback:[]},{silent:!0}),this.addMsg("","Window cleared"),this.view.render()},setMode:function(e){this.get("network").gateway.mode(this.get("name"),e)},isChannel:function(){return!0}}),m.model.Query=m.model.Channel.extend({initialize:function(){var e=this.get("name")||"";this.view=new m.view.Channel({model:this,name:e}),this.set({name:e,scrollback:[]},{silent:!0}),m.global.events.emit("panel:created",{panel:this})},isChannel:function(){return!1},isQuery:function(){return!0}}),m.model.Server=m.model.Channel.extend({initialize:function(){var e="Server";this.view=new m.view.Channel({model:this,name:e}),this.set({scrollback:[],name:e},{silent:!0}),m.global.events.emit("panel:created",{panel:this})},isServer:function(){return!0},isChannel:function(){return!1}}),m.model.Applet=m.model.Panel.extend({initialize:function(){var e="applet_"+(new Date).getTime().toString()+Math.ceil(100*Math.random()).toString();this.view=new m.view.Applet({model:this,name:e}),this.set({name:e},{silent:!0}),this.loaded_applet=null},load:function(e,t){return"object"==typeof e?(e.get||e.extend)&&(this.set("title",e.get("title")||m.global.i18n.translate("client_models_applet_unknown").fetch()),e.bind("change:title",function(e,t){this.set("title",t)},this),this.view.$el.html(""),e.view&&this.view.$el.append(e.view.$el),this.loaded_applet=e,this.loaded_applet.trigger("applet_loaded")):"string"==typeof e&&this.loadFromUrl(e,t),this},loadFromUrl:function(e,t){var n=this;this.view.$el.html(m.global.i18n.translate("client_models_applet_loading").fetch()),$script(e,function(){return m.applets[t]?void n.load(new m.applets[t]):void n.view.$el.html(m.global.i18n.translate("client_models_applet_notfound").fetch())})},close:function(){this.view.$el.remove(),this.destroy(),this.view=t,this.loaded_applet&&this.loaded_applet.dispose&&this.loaded_applet.dispose(),this.constructor.__super__.close.apply(this,arguments)},isApplet:function(){return!0}},{loadOnce:function(e){var t=_.find(m.app.panels("applets"),function(t){return t.isApplet()&&t.loaded_applet?t.loaded_applet.get("_applet_name")===e?!0:void 0:void 0});return t?t:this.load(e)},load:function(e,t){var n,i;return t=t||{},(i=this.getApplet(e))?(n=new m.model.Applet,n.load(new i({_applet_name:e})),t.no_tab||m.app.applet_panels.add(n),n):void 0},getApplet:function(e){return m.applets[e]||null},register:function(e,t){m.applets[e]=t}}),m.model.PluginManager=Backbone.Model.extend({initialize:function(){this.$plugin_holder=$('<div id="kiwi_plugins" style="display:none;"></div>').appendTo(m.app.view.$el),this.loading_plugins=0,this.loaded_plugins={}},load:function(e){var t=this;this.loaded_plugins[e]&&this.unload(e),this.loading_plugins++,this.loaded_plugins[e]=$("<div></div>"),this.loaded_plugins[e].appendTo(this.$plugin_holder).load(e,_.bind(t.pluginLoaded,t))},unload:function(e){this.loaded_plugins[e]&&(this.loaded_plugins[e].remove(),delete this.loaded_plugins[e])},pluginLoaded:function(){this.loading_plugins--,0===this.loading_plugins&&this.trigger("loaded")}}),m.model.DataStore=Backbone.Model.extend({initialize:function(){this._namespace="",this.new_data={}},namespace:function(e){return e&&(this._namespace=e),this._namespace},save:function(){localStorage.setItem(this._namespace,JSON.stringify(this.attributes))},load:function(){if(localStorage){var e;try{e=JSON.parse(localStorage.getItem(this._namespace))||{}}catch(t){e={}}this.attributes=e}}},{instance:function(e,t){var n=new m.model.DataStore(t);return n.namespace(e),n}}),m.model.ChannelInfo=Backbone.Model.extend({initialize:function(){this.view=new m.view.ChannelInfo({model:this})}}),m.view.Panel=Backbone.View.extend({tagName:"div",className:"panel",events:{},initialize:function(e){this.initializePanel(e)},initializePanel:function(e){this.$el.css("display","none"),e=e||{},this.$container=$(e.container?e.container:"#kiwi .panels .container1"),this.$el.appendTo(this.$container),this.alert_level=0,this.model.set({view:this},{silent:!0}),this.listenTo(this.model,"change:activity_counter",function(e,t){var n=this.model.tab.find(".activity");n.text(t>999?"999+":t),0===t?n.addClass("zero"):n.removeClass("zero")})},render:function(){},show:function(){var e=this.$el;this.$container.children(".panel").css("display","none"),e.css("display","block");var t=this.model.get("members");t?(m.app.rightbar.show(),t.view.show()):m.app.rightbar.hide(),this.alert("none"),this.model.set("activity_counter",0),m.app.panels.trigger("active",this.model,m.app.panels().active),this.model.trigger("active",this.model),m.app.view.doLayout(),this.model.isApplet()||this.scrollToBottom(!0)},alert:function(e){if(this.model!=m.app.panels().active){var t,n;t=["none","action","activity","highlight"],e=e||"none",n=_.indexOf(t,e),n||(e="none",n=0),0!==n&&n<=this.alert_level||(this.model.tab.removeClass(function(e,t){return(t.match(/\balert_\S+/g)||[]).join(" ")}),"none"!==e&&this.model.tab.addClass("alert_"+e),this.alert_level=n)}},scrollToBottom:function(e){this.model===m.app.panels().active&&(e||this.$container.scrollTop()+this.$container.height()>this.$el.outerHeight()-150)&&(this.$container[0].scrollTop=this.$container[0].scrollHeight)}}),m.view.Channel=m.view.Panel.extend({events:function(){var e=this.constructor.__super__.events;return _.isFunction(e)&&(e=e()),_.extend({},e,{"click .msg .nick":"nickClick","click .msg .inline-nick":"nickClick","click .chan":"chanClick","click .media .open":"mediaClick","mouseenter .msg .nick":"msgEnter","mouseleave .msg .nick":"msgLeave"})},initialize:function(e){this.initializePanel(e),this.$messages=$('<div class="messages"></div>'),this.$el.append(this.$messages),this.model.bind("change:topic",this.topic,this),this.model.bind("change:topic_set_by",this.topicSetBy,this),this.model.get("members")&&(this.model.get("members").bind("add",function(e){e.get("nick")===this.model.collection.network.get("nick")&&this.$el.find(".initial_loader").slideUp(function(){$(this).remove()})},this),this.model.get("members").bind("reset",function(e){e.getByNick(this.model.collection.network.get("nick"))&&this.$el.find(".initial_loader").slideUp(function(){$(this).remove()})},this)),this.model.isChannel()&&this.$el.append('<div class="initial_loader" style="margin:1em;text-align:center;"> '+m.global.i18n.translate("client_views_channel_joining").fetch()+' <span class="loader"></span></div>'),this.model.bind("msg",this.newMsg,this),this.msg_count=0},render:function(){var e=this;this.$messages.empty(),_.each(this.model.get("scrollback"),function(t){e.newMsg(t)})},newMsg:function(e){e=this.generateMessageDisplayObj(e),m.global.events.emit("message:display",{panel:this.model,message:e}).then(_.bind(function(){var t,n=_.clone(e);n.nick=u("message_nick",{nick:e.nick,prefix:e.nick_prefix||""}),t='<div class="msg <%= type %> <%= css_classes %>"><div class="time"><%- time_string %></div><div class="nick" style="<%= nick_style %>"><%- nick %></div><div class="text" style="<%= style %>"><%= msg %> </div></div>',this.$messages.append($(_.template(t,n)).data("message",e)),e.type.match(/^action /)?this.alert("action"):e.is_highlight?(m.app.view.alertWindow("* "+m.global.i18n.translate("client_views_panel_activity").fetch()),m.app.view.favicon.newHighlight(),m.app.view.playSound("highlight"),m.app.view.showNotification(this.model.get("name"),e.unparsed_msg),this.alert("highlight")):(this.model.isActive()&&m.app.view.alertWindow("* "+m.global.i18n.translate("client_views_panel_activity").fetch()),this.alert("activity")),this.model.isQuery()&&!this.model.isActive()&&(m.app.view.alertWindow("* "+m.global.i18n.translate("client_views_panel_activity").fetch()),e.is_highlight||m.app.view.favicon.newHighlight(),m.app.view.showNotification(this.model.get("name"),e.unparsed_msg),m.app.view.playSound("highlight")),function(){if(!this.model.isActive()){var t,n,i=m.global.settings.get("count_all_activity");"undefined"==typeof i&&(i=!1),t=["action join","action quit","action part","action kick","action nick","action mode"],(i||-1===_.indexOf(t,e.type))&&(n=this.model.get("activity_counter")||0,n++,this.model.set("activity_counter",n))}}.apply(this),this.model.isActive()&&this.scrollToBottom(),this.msg_count++,this.msg_count>(parseInt(m.global.settings.get("scrollback"),10)||250)&&($(".msg:first",this.$messages).remove(),this.msg_count--)},this))},parseMessageNicks:function(e,t){var n,i,s="";return n=this.model.get("members"),n&&(i=n.getByNick(e))?(t!==!1&&(s=this.getNickStyles(i.get("nick")).asCssString()),_.template('<span class="inline-nick" style="<%- style %>;cursor:pointer;" data-nick="<%- nick %>"><%- nick %></span>',{nick:e,style:s})):void 0},parseMessageChannels:function(e){var t,n=!1,i=this.model.get("network");if(i)return t=new RegExp("(^|\\s)(["+r(i.get("channel_prefix"))+"][^ ,\\007]+)","g"),e.match(t)?n=e.replace(t,function(e,t){return t+'<a class="chan" data-channel="'+_.escape(e.trim())+'">'+_.escape(e.trim())+"</a>"}):n},parseMessageUrls:function(e){var t,n=!1;return t=e.replace(/^(([A-Za-z][A-Za-z0-9\-]*\:\/\/)|(www\.))([\w.\-]+)([a-zA-Z]{2,6})(:[0-9]+)?(\/[\w!:.?$'()[\]*,;~+=&%@!\-\/]*)?(#.*)?$/gi,function(e){var t=e,i="";return e.match(/^javascript:/)?e:(n=!0,e.match(/^www\./)&&(e="http://"+e),t.length>100&&(t=t.substr(0,100)+"..."),i=m.view.MediaMessage.buildHtml(e),'<a class="link_ext" target="_blank" rel="nofollow" href="'+e.replace(/"/g,"%22")+'">'+_.escape(t)+"</a>"+i)}),n?t:!1},getNickStyles:function(e){var t,n,i,s,a=0;return _.map(e.split(""),function(e){a+=e.charCodeAt(0)}),s=(_.find(m.app.themes,function(e){return e.name.toLowerCase()===m.global.settings.get("theme").toLowerCase()})||{}).nick_lightness,s="number"!=typeof s?35:Math.max(0,Math.min(100,s)),i=o(a%255,70,s),i=i[2]|i[1]<<8|i[0]<<16,n="#"+i.toString(16),t={color:n},t.asCssString=function(){return _.reduce(this,function(e,t,n){return e+n+":"+t+";"},"")},t},generateMessageDisplayObj:function(e){var t,n,i,s,a,o,c=this.model.get("scrollback"),p=c[c.length-2];e=_.clone(e),e.css_classes="",e.nick_style="",e.is_highlight=!1,e.time_string="";var u=m.app.connections.active_connection.get("nick");return new RegExp("(^|\\W)("+r(u)+")(\\W|$)","i").test(e.msg)&&0!==e.nick.localeCompare(u)&&(e.is_highlight=!0,e.css_classes+=" highlight"),i=e.msg.split(" "),i=_.map(i,function(t){var n;return n=this.parseMessageUrls(t),"string"==typeof n?n:(n=this.parseMessageChannels(t),"string"==typeof n?n:(n=this.parseMessageNicks(t,"privmsg"===e.type),"string"==typeof n?n:(n=_.escape(t),m.global.settings.get("show_emoticons")&&(n=h(n)),n)))},this),e.unparsed_msg=e.msg,e.msg=i.join(" "),e.msg=l(e.msg),e.nick_style=this.getNickStyles(e.nick).asCssString(),t="",e.nick&&(_.map(e.nick.split(""),function(e){t+=e.charCodeAt(0).toString(16)}),e.css_classes+=" nick_"+t),p&&(n=(e.time.getTime()-p.time.getTime())/1e3/60,p.nick===e.nick&&1>n&&(e.css_classes+=" repeated_nick")),m.global.settings.get("use_24_hour_timestamps")?e.time_string=e.time.getHours().toString().lpad(2,"0")+":"+e.time.getMinutes().toString().lpad(2,"0")+":"+e.time.getSeconds().toString().lpad(2,"0"):(s=e.time.getHours(),a=s>11,s%=12,0===s&&(s=12),o=a?"client_views_panel_timestamp_pm":"client_views_panel_timestamp_am",e.time_string=d(o,s+":"+e.time.getMinutes().toString().lpad(2,"0")+":"+e.time.getSeconds().toString().lpad(2,"0"))),e},topic:function(e){"string"==typeof e&&e||(e=this.model.get("topic")),this.model.addMsg("",u("channel_topic",{text:e,channel:this.model.get("name")}),"topic"),m.app.panels().active===this.model&&m.app.topicbar.setCurrentTopicFromChannel(this.model)},topicSetBy:function(){m.app.panels().active===this.model&&m.app.topicbar.setCurrentTopicFromChannel(this.model)},nickClick:function(e){var t,n,i=$(e.currentTarget),s=this.model.get("members");e.stopPropagation(),t=i.data("nick"),t||(t=i.parent(".msg").data("message").nick),n=s?s.getByNick(t):null,n&&m.global.events.emit("nick:select",{target:i,member:n,source:"message"}).then(_.bind(this.openUserMenuForNick,this,i,n))},updateLastSeenMarker:function(){this.model.isActive()&&(this.$(".last_seen").removeClass("last_seen"),this.$messages.children().last().addClass("last_seen"))},openUserMenuForNick:function(e,t){var n,i,s=this.model.get("members"),a=!!s.getByNick(m.app.connections.active_connection.get("nick")).get("is_op");n=new m.view.UserBox,n.setTargets(t,this.model),n.displayOpItems(a),i=new m.view.MenuBox(t.get("nick")||"User"),i.addItem("userbox",n.$el),i.showFooter(!1),m.global.events.emit("usermenu:created",{menu:i,userbox:n,user:t}).then(_.bind(function(){i.show();var t=e.offset(),n=t.top,s=n+i.$el.outerHeight(),a=this.$el.parent().offset().top+this.$el.parent().outerHeight();s>a&&(n=a-i.$el.outerHeight()),i.$el.offset({left:t.left,top:n})},this)).catch(_.bind(function(){n=null,menu.dispose(),menu=null},this))},chanClick:function(e){var t=e.target?$(e.target).data("channel"):$(e.srcElement).data("channel");m.app.connections.active_connection.gateway.join(t)},mediaClick:function(e){var t,n=$(e.target).parents(".media");n.data("media")?t=n.data("media"):(t=new m.view.MediaMessage({el:n[0]}),n.data("media",t)),t.toggle()},msgEnter:function(e){var t;_.each($(e.currentTarget).parent(".msg").attr("class").split(" "),function(e){e.match(/^nick_[a-z0-9]+/i)&&(t=e)}),t&&$("."+t).addClass("global_nick_highlight")},msgLeave:function(e){var t;_.each($(e.currentTarget).parent(".msg").attr("class").split(" "),function(e){e.match(/^nick_[a-z0-9]+/i)&&(t=e)}),t&&$("."+t).removeClass("global_nick_highlight")}}),m.view.Applet=m.view.Panel.extend({className:"panel applet",initialize:function(e){this.initializePanel(e)}}),m.view.Application=Backbone.View.extend({initialize:function(){var e=this;this.$el=$($("#tmpl_application").html().trim()),this.el=this.$el[0],$(this.model.get("container")||"body").append(this.$el),this.elements={panels:this.$el.find(".panels"),right_bar:this.$el.find(".right_bar"),toolbar:this.$el.find(".toolbar"),controlbox:this.$el.find(".controlbox"),resize_handle:this.$el.find(".memberlists_resize_handle")},$(window).resize(function(){e.doLayout.apply(e)}),this.elements.toolbar.resize(function(){e.doLayout.apply(e)}),this.elements.controlbox.resize(function(){e.doLayout.apply(e)}),m.global.settings.on("change:theme",this.updateTheme,this),this.updateTheme(getQueryVariable("theme")),m.global.settings.on("change:channel_list_style",this.setTabLayout,this),this.setTabLayout(m.global.settings.get("channel_list_style")),m.global.settings.on("change:show_timestamps",this.displayTimestamps,this),this.displayTimestamps(m.global.settings.get("show_timestamps")),this.$el.appendTo($("body")),this.doLayout(),$(document).keydown(this.setKeyFocus),window.onbeforeunload=function(){return m.gateway.isConnected()?m.global.i18n.translate("client_views_application_close_notice").fetch():void 0},this.has_focus=!0,$(window).on("focus",function(){e.has_focus=!0}),$(window).on("blur",function(){var t=e.model.panels().active;t&&t.view.updateLastSeenMarker&&t.view.updateLastSeenMarker(),e.has_focus=!1}),$(window).on("touchstart",function t(){e.$el.addClass("touch"),$(window).off("touchstart",t)}),this.favicon=new m.view.Favicon,this.initSound(),this.monitorPanelFallback()},updateTheme:function(e){e===m.global.settings&&(e=arguments[1]),e||(e=m.global.settings.get("theme")||"relaxed"),e=e.toLowerCase(),$("[data-theme]:not([disabled])").each(function(e,t){var n=$(t);n.attr("rel","alternate "+n.attr("rel")).attr("disabled",!0)[0].disabled=!0});var t=$("[data-theme][title="+e+"]");t.length>0&&(t.attr("rel","stylesheet").attr("disabled",!1)[0].disabled=!1),this.doLayout()},setTabLayout:function(e){e===m.global.settings&&(e=arguments[1]),"list"==e?this.$el.addClass("chanlist_treeview"):this.$el.removeClass("chanlist_treeview"),this.doLayout()},displayTimestamps:function(e){e===m.global.settings&&(e=arguments[1]),e?this.$el.addClass("timestamps"):this.$el.removeClass("timestamps")},setKeyFocus:function(e){e.ctrlKey||e.altKey||e.metaKey||"input"===e.target.tagName.toLowerCase()||"textarea"===e.target.tagName.toLowerCase()||$(e.target).attr("contenteditable")||$("#kiwi .controlbox .inp").focus()},doLayout:function(){var e=this.$el,t=this.elements.panels,n=this.elements.right_bar,i=this.elements.toolbar,s=this.elements.controlbox,a=this.elements.resize_handle;if(e.is(":visible")){var o={top:i.outerHeight(!0),bottom:s.outerHeight(!0)};i.is(":visible")||(o.top=0),s.is(":visible")||(o.bottom=0),t.css(o),n.css(o),a.css(o),e.hasClass("chanlist_treeview")&&this.$el.find(".tabs",e).css(o),e.outerWidth()<420?(e.addClass("narrow"),this.model.rightbar&&this.model.rightbar.keep_hidden!==!0&&this.model.rightbar.toggle(!0)):(e.removeClass("narrow"),this.model.rightbar&&this.model.rightbar.keep_hidden!==!1&&this.model.rightbar.toggle(!1)),n.hasClass("disabled")?(t.css("right",0),a.css("left",t.outerWidth(!0))):(t.css("right",n.outerWidth(!0)),a.css("left",n.position().left-a.outerWidth(!0)/2));var c=parseInt(s.find(".input_tools").outerWidth(),10);s.find(".input_wrap").css("right",c+7)}},alertWindow:function(e){this.alertWindowTimer||(this.alertWindowTimer=new function(){var e,t=this,n=!0,i=0,s=m.app.server_settings.client.window_title||"Kiwi IRC",a="Kiwi IRC";this.setTitle=function(e){return e=e||s,window.document.title=e,e},this.start=function(t){n||(a=t,e||(e=setInterval(this.update,1e3)))},this.stop=function(){e&&clearInterval(e),e=null,this.setTitle(),setTimeout(this.reset,2e3)},this.reset=function(){e||t.setTitle()},this.update=function(){0===i?(t.setTitle(a),i=1):(t.setTitle(),i=0)},$(window).focus(function(){n=!0,t.stop(),setTimeout(t.reset,2e3)}),$(window).blur(function(){n=!1})}),this.alertWindowTimer.start(e)},barsHide:function(e){e?(this.$el.find(".toolbar").slideUp(0),$("#kiwi .controlbox").slideUp(0),this.doLayout()):(this.$el.find(".toolbar").slideUp({queue:!1,duration:400,step:$.proxy(this.doLayout,this)}),$("#kiwi .controlbox").slideUp({queue:!1,duration:400,step:$.proxy(this.doLayout,this)}))},barsShow:function(e){e?(this.$el.find(".toolbar").slideDown(0),$("#kiwi .controlbox").slideDown(0),this.doLayout()):(this.$el.find(".toolbar").slideDown({queue:!1,duration:400,step:$.proxy(this.doLayout,this)}),$("#kiwi .controlbox").slideDown({queue:!1,duration:400,step:$.proxy(this.doLayout,this)}))},initSound:function(){var e=this,t=this.model.get("base_path");$script(t+"/assets/libs/soundmanager2/soundmanager2-nodebug-jsmin.js",function(){"undefined"!=typeof soundManager&&soundManager.setup({url:t+"/assets/libs/soundmanager2/",flashVersion:9,preferFlash:!0,onready:function(){e.sound_object=soundManager.createSound({id:"highlight",url:t+"/assets/sound/highlight.mp3"})}})})},playSound:function(e){this.sound_object&&(m.global.settings.get("mute_sounds")||soundManager.play(e))},showNotification:function(e,t){var n=this.model.get("base_path")+"/assets/img/ico.png",i=m.utils.notifications;!this.has_focus&&i.allowed()&&i.create(e,{icon:n,body:t}).closeAfter(5e3).on("click",_.bind(window.focus,window))},monitorPanelFallback:function(){var e=[];this.model.panels.on("active",function(){var t,n=m.app.panels().active;t=_.indexOf(e,n.cid),t>-1&&e.splice(t,1),e.unshift(n.cid)}),this.model.panels.on("remove",function(t){if(e[0]===t.cid){e.shift();var n=_.find(m.app.panels("applets").concat(m.app.panels("connections")),{cid:e[0]});n&&n.view.show()}})}}),m.view.AppToolbar=Backbone.View.extend({events:{"click .settings":"clickSettings","click .startup":"clickStartup"},initialize:function(){m.app.server_settings.connection&&!m.app.server_settings.connection.allow_change&&this.$(".startup").css("display","none")},clickSettings:function(e){e.preventDefault(),m.app.controlbox.processInput("/settings")},clickStartup:function(e){e.preventDefault(),m.app.startup_applet.view.show()}}),m.view.ControlBox=Backbone.View.extend({events:{"keydown .inp":"process","click .nick":"showNickChange"},initialize:function(){var e=this;this.buffer=[],this.buffer_pos=0,this.preprocessor=new a,this.preprocessor.recursive_depth=5,this.tabcomplete={active:!1,data:[],prefix:""},m.app.connections.on("change:nick",function(t){t===m.app.connections.active_connection&&$(".nick",e.$el).text(t.get("nick"))}),m.app.connections.on("active",function(t,n){$(".nick",e.$el).text(n.get("nick"))}),m.app.panels.bind("active",function(t){(t.isChannel()||t.isServer()||t.isQuery())&&e.$(".inp").focus()})},render:function(){var e=d("client_views_controlbox_message");return this.$(".inp").attr("placeholder",e),this},showNickChange:function(){this.nick_change||(this.nick_change=new m.view.NickChangeBox,this.nick_change.render(),this.listenTo(this.nick_change,"close",function(){delete this.nick_change}))},process:function(e){var t,n=this,i=$(e.currentTarget),s=i.val();switch(t=-1!==navigator.appVersion.indexOf("Mac")?e.metaKey:e.altKey,this.tabcomplete.active&&9!==e.keyCode&&(this.tabcomplete.active=!1,this.tabcomplete.data=[],this.tabcomplete.prefix=""),!0){case 13===e.keyCode:return s=s.trim(),s&&($.each(s.split("\n"),function(e,t){n.processInput(t)}),this.buffer.push(s),this.buffer_pos=this.buffer.length),i.val(""),!1;case 38===e.keyCode:return this.buffer_pos>0&&(this.buffer_pos--,i.val(this.buffer[this.buffer_pos])),!1;case 40===e.keyCode:this.buffer_pos<this.buffer.length&&(this.buffer_pos++,i.val(this.buffer[this.buffer_pos]));break;case 219===e.keyCode&&t:var a=$("#kiwi .tabs").find("li[class!=connection]"),o=function(){for(var e=0;e<a.length;e++)if($(a[e]).hasClass("active"))return e}();return $prev_tab=$(0===o?a[a.length-1]:a[o-1]),$prev_tab.click(),!1;case 221===e.keyCode&&t:var a=$("#kiwi .tabs").find("li[class!=connection]"),o=function(){for(var e=0;e<a.length;e++)if($(a[e]).hasClass("active"))return e
+}();return $next_tab=$(o===a.length-1?a[0]:a[o+1]),$next_tab.click(),!1;case!(9!==e.keyCode||e.shiftKey||e.altKey||e.metaKey||e.ctrlKey):if(this.tabcomplete.active=!0,_.isEqual(this.tabcomplete.data,[])){var c=[],l=m.app.panels().active.get("members");l=l?l.models:[],$.each(l,function(e,t){t&&c.push(t.get("nick"))}),c.push(m.app.panels().active.get("name")),c=_.sortBy(c,function(e){return e.toLowerCase()}),this.tabcomplete.data=c}return" "===s[i[0].selectionStart-1]?!1:(function(){var e,t,a,o,c,l,r=": ";e=s.substring(0,i[0].selectionStart).split(" "),":"==e[e.length-1]&&e.pop(),e.length>1&&(r=""),l=e[e.length-1],""===this.tabcomplete.prefix&&(this.tabcomplete.prefix=l),this.tabcomplete.data=_.select(this.tabcomplete.data,function(e){return 0===e.toLowerCase().indexOf(n.tabcomplete.prefix.toLowerCase())}),this.tabcomplete.data.length>0&&(a=i[0].selectionStart-l.length,t=s.substr(0,a),o=this.tabcomplete.data.shift(),this.tabcomplete.data.push(o),t+=o,s.substr(i[0].selectionStart,2)!==r&&(t+=r),t+=s.substr(i[0].selectionStart),i.val(t),i[0].setSelectionRange?i[0].setSelectionRange(a+o.length+r.length,a+o.length+r.length):i[0].createTextRange&&(c=i[0].createTextRange(),c.collapse(!0),c.moveEnd("character",a+o.length+r.length),c.moveStart("character",a+o.length+r.length),c.select()))}.apply(this),!1)}},processInput:function(e){var t,n,i,s=this;"/"===e[0]||m.app.panels().active.isChannel()||m.app.panels().active.isQuery()||(e="/"+e),("/"!==e[0]||"//"===e.substr(0,2))&&(e=e.replace(/^\/\//,"/"),e="/msg "+m.app.panels().active.get("name")+" "+e),this.preprocessor.vars.server=m.app.connections.active_connection.get("name"),this.preprocessor.vars.channel=m.app.panels().active.get("name"),this.preprocessor.vars.destination=this.preprocessor.vars.channel,e=this.preprocessor.process(e),n=e.split(/\s/),"/"===n[0][0]?(t=n[0].substr(1).toLowerCase(),n=n.splice(1,n.length-1)):(t="msg",n.unshift(m.app.panels().active.get("name"))),i={command:t,params:n},m.global.events.emit("command",i).then(function(){s.trigger("command",{command:i.command,params:i.params}),s.trigger("command:"+i.command,{command:i.command,params:i.params}),s._events["command:"+i.command]||s.trigger("unknown_command",{command:i.command,params:i.params})})},addPluginIcon:function(e){var t=$('<div class="tool"></div>').append(e);this.$el.find(".input_tools").append(t),m.app.view.doLayout()}}),m.view.Favicon=Backbone.View.extend({initialize:function(){var e=this,t=$(window);this.has_focus=!0,this.highlight_count=0,this.has_canvas_support=!!window.CanvasRenderingContext2D,this.original_favicon=$('link[rel~="icon"]')[0].href,this._createCanvas(),t.on("focus",function(){e.has_focus=!0,e._resetHighlights()}),t.on("blur",function(){e.has_focus=!1})},newHighlight:function(){var e=this;this.has_focus||(this.highlight_count++,this.has_canvas_support&&this._drawFavicon(function(){e._drawBubble(e.highlight_count.toString()),e._refreshFavicon(e.canvas.toDataURL())}))},_resetHighlights:function(){this.highlight_count=0,this._refreshFavicon(this.original_favicon)},_drawFavicon:function(e){var t=this.canvas,n=t.getContext("2d"),i=new Image;i.crossOrigin="anonymous",i.src=this.original_favicon,i.onload=function(){n.clearRect(0,0,t.width,t.height),n.drawImage(i,0,0,t.width,t.height),e()}},_drawBubble:function(e){var t,n=0,i=0,s=this.canvas,a=test_context=s.getContext("2d"),o=s.width,c=s.height;t=-1!==navigator.appVersion.indexOf("Mac")?-1.5:-1,test_context.font=a.font="bold 10px Arial",test_context.textAlign="right",this._renderText(test_context,e,0,0,t),n=test_context.measureText(e).width+t*(e.length-1)+2,i=9,bubbleX=o-n,bubbleY=c-i,a.fillStyle="red",a.fillRect(bubbleX,bubbleY,n,i),a.fillStyle="white",this._renderText(a,e,o-1,c-1,t)},_refreshFavicon:function(e){$('link[rel~="icon"]').remove(),$('<link rel="shortcut icon" href="'+e+'">').appendTo($("head"))},_createCanvas:function(){var e=document.createElement("canvas");e.width=16,e.height=16,this.canvas=e},_renderText:function(e,t,n,i,s){for(var a,o=t.split("").reverse(),c=0,l=n;c<t.length;)a=o[c++],e.fillText(a,l,i),l+=-1*(e.measureText(a).width+s);return e}}),m.view.MediaMessage=Backbone.View.extend({events:{"click .media_close":"close"},initialize:function(){this.url=this.$el.data("url")},toggle:function(){this.$content&&this.$content.is(":visible")?this.close():this.open()},close:function(){var e=this;this.$content.slideUp("fast",function(){e.$content.remove()})},open:function(){this.$content||(this.$content=$('<div class="media_content"><a class="media_close"><i class="fa fa-chevron-up"></i> '+m.global.i18n.translate("client_views_mediamessage_close").fetch()+'</a><br /><div class="content"></div></div>'),this.$content.find(".content").append(this.mediaTypes[this.$el.data("type")].apply(this,[])||m.global.i18n.translate("client_views_mediamessage_notfound").fetch()+" :(")),this.$content.is(":visible")||(this.$content.hide(),this.$el.append(this.$content),this.$content.slideDown())},mediaTypes:{twitter:function(){var e=this.$el.data("tweetid"),t=this;return $.getJSON("https://api.twitter.com/1/statuses/oembed.json?id="+e+"&callback=?",function(e){t.$content.find(".content").html(e.html)}),$("<div>"+m.global.i18n.translate("client_views_mediamessage_load_tweet").fetch()+"...</div>")},image:function(){return $('<a href="'+this.url+'" target="_blank"><img height="100" src="'+this.url+'" /></a>')},imgur:function(){var e=this;return $.getJSON("http://api.imgur.com/oembed?url="+this.url,function(t){var n='<a href="'+t.url+'" target="_blank"><img height="100" src="'+t.url+'" /></a>';e.$content.find(".content").html(n)}),$("<div>"+m.global.i18n.translate("client_views_mediamessage_load_image").fetch()+"...</div>")},reddit:function(){var e=this,t=/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi.exec(this.url);return $.getJSON("http://www."+t[0]+".json?jsonp=?",function(t){console.log("Loaded reddit data",t);var n=t[0].data.children[0].data,i="";n.thumbnail&&(n.over_18?(i="<span class=\"thumbnail_nsfw\" onclick=\"$(this).find('p').remove(); $(this).find('img').css('visibility', 'visible');\">",i+='<p style="font-size:0.9em;line-height:1.2em;cursor:pointer;">Show<br />NSFW</p>',i+='<img src="'+n.thumbnail+'" class="thumbnail" style="visibility:hidden;" />',i+="</span>"):i='<img src="'+n.thumbnail+'" class="thumbnail" />');var s="<div>"+i+"<b><%- title %></b><br />Posted by <%- author %>. &nbsp;&nbsp; ";s+='<i class="fa fa-arrow-up"></i> <%- ups %> &nbsp;&nbsp; <i class="fa fa-arrow-down"></i> <%- downs %><br />',s+='<%- num_comments %> comments made. <a href="http://www.reddit.com<%- permalink %>">View post</a></div>',e.$content.find(".content").html(_.template(s,n))}),$("<div>"+m.global.i18n.translate("client_views_mediamessage_load_reddit").fetch()+"...</div>")},youtube:function(){var e=this.$el.data("ytid"),t=this,n='<iframe width="480" height="270" src="https://www.youtube.com/embed/'+e+'?feature=oembed" frameborder="0" allowfullscreen=""></iframe>';return t.$content.find(".content").html(n),$("")},gist:function(){var e=this,t=/https?:\/\/gist\.github\.com\/(?:[a-z0-9-]*\/)?([a-z0-9]+)(\#(.+))?$/i.exec(this.url);return $.getJSON("https://gist.github.com/"+t[1]+".json?callback=?"+(t[2]||""),function(t){$("body").append('<link rel="stylesheet" href="'+t.stylesheet+'" type="text/css" />'),e.$content.find(".content").html(t.div)}),$("<div>"+m.global.i18n.translate("client_views_mediamessage_load_gist").fetch()+"...</div>")},spotify:function(){var e,t,n=this.$el.data("uri"),i=this.$el.data("method");switch(i){case"track":case"album":e={url:"https://embed.spotify.com/?uri="+n,width:300,height:80};break;case"artist":e={url:"https://embed.spotify.com/follow/1/?uri="+n+"&size=detail&theme=dark",width:300,height:56}}return t='<iframe src="'+e.url+'" width="'+e.width+'" height="'+e.height+'" frameborder="0" allowtransparency="true"></iframe>',$(t)},soundcloud:function(){var e=this.$el.data("url"),t=$("<div></div>").text(m.global.i18n.translate("client_models_applet_loading").fetch());return $.getJSON("https://soundcloud.com/oembed",{url:e}).then(function(e){t.empty().append($(e.html).attr("height",e.height-100))},function(){t.text(m.global.i18n.translate("client_views_mediamessage_notfound").fetch())}),t},custom:function(){var e=this.constructor.types[this.$el.data("index")];if(e)return $(e.buildHtml(this.$el.data("url")))}}},{addType:function(e,t){"function"==typeof e&&"function"==typeof t&&(this.types=this.types||[],this.types.push({match:e,buildHtml:t}))},buildHtml:function(e){var t,n="";if(_.each(this.types||[],function(t,i){t.match(e)&&(n+='<span class="media" title="Open" data-type="custom" data-index="'+i+'" data-url="'+_.escape(e)+'"><a class="open"><i class="fa fa-chevron-right"></i></a></span>')}),e.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)&&(n+='<span class="media image" data-type="image" data-url="'+e+'" title="Open Image"><a class="open"><i class="fa fa-chevron-right"></i></a></span>'),t=/imgur\.com\/[^\/]*(?!=\.[^!.]+($|\?))/gi.exec(e),t&&!e.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)&&(n+='<span class="media imgur" data-type="imgur" data-url="'+e+'" title="Open Image"><a class="open"><i class="fa fa-chevron-right"></i></a></span>'),t=/https?:\/\/twitter.com\/([a-zA-Z0-9_]+)\/status\/([0-9]+)/gi.exec(e),t&&(n+='<span class="media twitter" data-type="twitter" data-url="'+e+'" data-tweetid="'+t[2]+'" title="Show tweet information"><a class="open"><i class="fa fa-chevron-right"></i></a></span>'),t=/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi.exec(e),t&&(n+='<span class="media reddit" data-type="reddit" data-url="'+e+'" title="Reddit thread"><a class="open"><i class="fa fa-chevron-right"></i></a></span>'),t=/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/gi.exec(e),t&&(n+='<span class="media youtube" data-type="youtube" data-url="'+e+'" data-ytid="'+t[1]+'" title="YouTube Video"><a class="open"><i class="fa fa-chevron-right"></i></a></span>'),t=/https?:\/\/gist\.github\.com\/(?:[a-z0-9-]*\/)?([a-z0-9]+)(\#(.+))?$/i.exec(e),t&&(n+='<span class="media gist" data-type="gist" data-url="'+e+'" data-gist_id="'+t[1]+'" title="GitHub Gist"><a class="open"><i class="fa fa-chevron-right"></i></a></span>'),t=/http:\/\/(?:play|open\.)?spotify.com\/(album|track|artist)\/([a-zA-Z0-9]+)\/?/i.exec(e)){var i=t[1],s="spotify:"+t[1]+":"+t[2];n+='<span class="media spotify" data-type="spotify" data-uri="'+s+'" data-method="'+i+'" title="Spotify '+i+'"><a class="open"><i class="fa fa-chevron-right"></i></a></span>'}return t=/(?:m\.)?(soundcloud\.com(?:\/.+))/i.exec(e),t&&(n+='<span class="media soundcloud" data-type="soundcloud" data-url="http://'+t[1]+'" title="SoundCloud player"><a class="open"><i class="fa fa-chevron-right"></i></a></span>'),n}}),m.view.Member=Backbone.View.extend({tagName:"li",initialize:function(){this.model.bind("change",this.render,this),this.render()},render:function(){var e=this.$el,t=(this.model.get("modes")||[]).join(" ");return e.attr("class","mode "+t),e.html('<a class="nick"><span class="prefix">'+this.model.get("prefix")+"</span>"+this.model.get("nick")+"</a>"),this}}),m.view.MemberList=Backbone.View.extend({tagName:"div",events:{"click .nick":"nickClick","click .channel_info":"channelInfoClick"},initialize:function(){this.model.bind("all",this.render,this),this.$el.appendTo("#kiwi .memberlists"),this.$meta=$('<div class="meta"></div>').appendTo(this.$el),this.$list=$("<ul></ul>").appendTo(this.$el)},render:function(){var e=this;return this.$list.empty(),this.model.forEach(function(t){t.view.$el.data("member",t),e.$list.append(t.view.$el)}),this.model.channel.isActive()&&this.renderMeta(),this},renderMeta:function(){var e=this.model.length+" "+d("client_applets_chanlist_users");this.$meta.text(e)},nickClick:function(e){var t=$(e.currentTarget).parent("li"),n=t.data("member");m.global.events.emit("nick:select",{target:t,member:n,source:"nicklist"}).then(_.bind(this.openUserMenuForItem,this,t))},openUserMenuForItem:function(e){var t,n=e.data("member"),i=!!this.model.getByNick(m.app.connections.active_connection.get("nick")).get("is_op");t=new m.view.UserBox,t.setTargets(n,this.model.channel),t.displayOpItems(i);var s=new m.view.MenuBox(n.get("nick")||"User");s.addItem("userbox",t.$el),s.showFooter(!1),m.global.events.emit("usermenu:created",{menu:s,userbox:t,user:n}).then(_.bind(function(){s.show();var t=e.offset(),n=t.top,i=n+s.$el.outerHeight(),a=this.$el.parent().offset().top+this.$el.parent().outerHeight(),o=t.left,c=o+s.$el.outerWidth(),l=this.$el.parent().offset().left+this.$el.parent().outerWidth();i>a&&(n=a-s.$el.outerHeight()),0>n&&(n=0),c>l&&(o=l-s.$el.outerWidth()),s.$el.offset({left:o,top:n})},this)).catch(_.bind(function(){t=null,s.dispose(),s=null},this))},channelInfoClick:function(){new m.model.ChannelInfo({channel:this.model.channel})},show:function(){$("#kiwi .memberlists").children().removeClass("active"),$(this.el).addClass("active"),this.renderMeta()}}),m.view.MenuBox=Backbone.View.extend({events:{"click .ui_menu_foot .close, a.close_menu":"dispose"},initialize:function(e){this.$el=$('<div class="ui_menu"><div class="items"></div></div>'),this._title=e||"",this._items={},this._display_footer=!0,this._close_on_blur=!0},render:function(){var e,t=this,n=t.$el.find(".items");n.find("*").remove(),this._title&&(e=$('<div class="ui_menu_title"></div>').text(this._title),this.$el.prepend(e)),_.each(this._items,function(e){var t=$('<div class="ui_menu_content hover"></div>').append(e);n.append(t)}),this._display_footer&&this.$el.append('<div class="ui_menu_foot"><a class="close" onclick="">Close <i class="fa fa-times"></i></a></div>')},setTitle:function(e){this._title=e,this._title&&this.$el.find(".ui_menu_title").text(this._title)},onDocumentClick:function(e){var t=$(e.target);this._close_on_blur&&t[0]!=this.$el[0]&&0===this.$el.has(t).length&&this.dispose()},dispose:function(){_.each(this._items,function(e){e.dispose&&e.dispose(),e.remove&&e.remove()}),this._items=null,this.remove(),this._close_proxy&&$(document).off("click",this._close_proxy)},addItem:function(e,t){t.is("a")&&t.addClass("fa fa-chevron-right"),this._items[e]=t},removeItem:function(e){delete this._items[e]},showFooter:function(e){this._display_footer=e},closeOnBlur:function(e){this._close_on_blur=e},show:function(){var e,t,n=this;this.render(),this.$el.appendTo(m.app.view.$el),e=m.app.view.$el.find(".controlbox"),$items=this.$el.find(".items"),t=this.$el.outerHeight()-$items.outerHeight(),$items.css({"overflow-y":"auto","max-height":e.offset().top-this.$el.offset().top-t}),setTimeout(function(){n._close_proxy=function(e){n.onDocumentClick(e)},$(document).on("click",n._close_proxy)},0)}}),m.view.NetworkTabs=Backbone.View.extend({tagName:"ul",className:"connections",initialize:function(){this.model.on("add",this.networkAdded,this),this.model.on("remove",this.networkRemoved,this),this.$el.appendTo(m.app.view.$el.find(".tabs"))},networkAdded:function(e){$('<li class="connection"></li>').append(e.panels.view.$el).appendTo(this.$el)},networkRemoved:function(e){e.panels.view.$el.parent().remove(),e.panels.view.remove(),m.app.view.doLayout()}}),m.view.NickChangeBox=Backbone.View.extend({events:{submit:"changeNick","click .cancel":"close"},initialize:function(){var e={new_nick:m.global.i18n.translate("client_views_nickchangebox_new").fetch(),change:m.global.i18n.translate("client_views_nickchangebox_change").fetch(),cancel:m.global.i18n.translate("client_views_nickchangebox_cancel").fetch()};this.$el=$(_.template($("#tmpl_nickchange").html().trim(),e))},render:function(){m.app.controlbox.$el.prepend(this.$el),this.$el.find("input").focus(),this.$el.css("bottom",m.app.controlbox.$el.outerHeight(!0))},close:function(){this.$el.remove(),this.trigger("close")},changeNick:function(e){e.preventDefault();var t=m.app.connections.active_connection;this.listenTo(t,"change:nick",function(){this.close()}),t.gateway.changeNick(this.$("input").val())}}),m.view.ResizeHandler=Backbone.View.extend({events:{mousedown:"startDrag",mouseup:"stopDrag"},initialize:function(){this.dragging=!1,this.starting_width={},$(window).on("mousemove",$.proxy(this.onDrag,this))},startDrag:function(){this.dragging=!0},stopDrag:function(){this.dragging=!1},onDrag:function(e){if(this.dragging){var t=$("#kiwi").offset().left;this.$el.css("left",e.clientX-this.$el.outerWidth(!0)/2-t),$("#kiwi .right_bar").css("width",this.$el.parent().width()-(this.$el.position().left+this.$el.outerWidth())),m.app.view.doLayout()}}}),m.view.ServerSelect=Backbone.View.extend({events:{"submit form":"submitForm","click .show_more":"showMore","change .have_pass input":"showPass","change .have_key input":"showKey","click .fa-key":"channelKeyIconClick","click .show_server":"showServer"},initialize:function(){var e={think_nick:m.global.i18n.translate("client_views_serverselect_form_title").fetch(),nickname:m.global.i18n.translate("client_views_serverselect_nickname").fetch(),have_password:m.global.i18n.translate("client_views_serverselect_enable_password").fetch(),password:m.global.i18n.translate("client_views_serverselect_password").fetch(),channel:m.global.i18n.translate("client_views_serverselect_channel").fetch(),channel_key:m.global.i18n.translate("client_views_serverselect_channelkey").fetch(),require_key:m.global.i18n.translate("client_views_serverselect_channelkey_required").fetch(),key:m.global.i18n.translate("client_views_serverselect_key").fetch(),start:m.global.i18n.translate("client_views_serverselect_connection_start").fetch(),server_network:m.global.i18n.translate("client_views_serverselect_server_and_network").fetch(),server:m.global.i18n.translate("client_views_serverselect_server").fetch(),port:m.global.i18n.translate("client_views_serverselect_port").fetch(),powered_by:m.global.i18n.translate("client_views_serverselect_poweredby").fetch()};this.$el=$(_.template($("#tmpl_server_select").html().trim(),e)),m.app.server_settings&&m.app.server_settings.connection&&(m.app.server_settings.connection.allow_change||(this.$el.find(".show_more").remove(),this.$el.addClass("single_server"))),this.state="all",this.more_shown=!1,this.model.bind("new_network",this.newNetwork,this),this.gateway=m.global.components.Network(),this.gateway.on("connect",this.networkConnected,this),this.gateway.on("connecting",this.networkConnecting,this),this.gateway.on("disconnect",this.networkDisconnected,this),this.gateway.on("irc_error",this.onIrcError,this)},dispose:function(){this.model.off("new_network",this.newNetwork,this),this.gateway.off(),this.remove()},submitForm:function(e){return e.preventDefault(),$("input.nick",this.$el).val().trim()?("nick_change"===this.state?this.submitNickChange(e):this.submitLogin(e),void $("button",this.$el).attr("disabled",1)):(this.setStatus(m.global.i18n.translate("client_views_serverselect_nickname_error_empty").fetch()),void $("input.nick",this.$el).select())},submitLogin:function(){if(!$("button",this.$el).attr("disabled")){var e={nick:$("input.nick",this.$el).val(),server:$("input.server",this.$el).val(),port:$("input.port",this.$el).val(),ssl:$("input.ssl",this.$el).prop("checked"),password:$("input.password",this.$el).val(),channel:$("input.channel",this.$el).val(),channel_key:$("input.channel_key",this.$el).val(),options:this.server_options};this.trigger("server_connect",e)}},submitNickChange:function(){m.gateway.changeNick(null,$("input.nick",this.$el).val()),this.networkConnecting()},showPass:function(){this.$el.find("tr.have_pass input").is(":checked")?this.$el.find("tr.pass").show().find("input").focus():this.$el.find("tr.pass").hide().find("input").val("")},channelKeyIconClick:function(){this.$el.find("tr.have_key input").click()},showKey:function(){this.$el.find("tr.have_key input").is(":checked")?this.$el.find("tr.key").show().find("input").focus():this.$el.find("tr.key").hide().find("input").val("")},showMore:function(){this.more_shown?($(".more",this.$el).slideUp("fast"),$(".show_more",this.$el).children(".fs-caret-up").removeClass("fa-caret-up").addClass("fa-caret-down"),$("input.nick",this.$el).select(),this.more_shown=!1):($(".more",this.$el).slideDown("fast"),$(".show_more",this.$el).children(".fa-caret-down").removeClass("fa-caret-down").addClass("fa-caret-up"),$("input.server",this.$el).select(),this.more_shown=!0)},populateFields:function(e){var t,n,i,s,a,o,c;e=e||{},t=e.nick||"",n=e.server||"",i=e.port||6667,o=e.ssl||0,c=e.password||"",s=e.channel||"",a=e.channel_key||"",$("input.nick",this.$el).val(t),$("input.server",this.$el).val(n),$("input.port",this.$el).val(i),$("input.ssl",this.$el).prop("checked",o),$("input#server_select_show_pass",this.$el).prop("checked",!!c),$("input.password",this.$el).val(c),c&&$("tr.pass",this.$el).show(),$("input.channel",this.$el).val(s),$("input#server_select_show_channel_key",this.$el).prop("checked",!!a),$("input.channel_key",this.$el).val(a),a&&$("tr.key",this.$el).show(),this.server_options={},e.encoding&&(this.server_options.encoding=e.encoding)},hide:function(){this.$el.slideUp()},show:function(e){e=e||"all",this.$el.show(),"all"===e?$(".show_more",this.$el).show():"more"===e?$(".more",this.$el).slideDown("fast"):"nick_change"===e?($(".more",this.$el).hide(),$(".show_more",this.$el).hide(),$("input.nick",this.$el).select()):"enter_password"===e&&($(".more",this.$el).hide(),$(".show_more",this.$el).hide(),$("input.password",this.$el).select()),this.state=e},infoBoxShow:function(){var e=this.$el.find(".side_panel");e.is(":visible")&&this.$el.animate({width:parseInt(e.css("left"),10)+e.find(".content:first").outerWidth()})},infoBoxHide:function(){var e=this.$el.find(".side_panel");this.$el.animate({width:parseInt(e.css("left"),10)})},infoBoxSet:function(e){this.$el.find(".side_panel .content").empty().append(e)},setStatus:function(e,t){$(".status",this.$el).text(e).attr("class","status").addClass(t||"").show()},clearStatus:function(){$(".status",this.$el).hide()},reset:function(){this.populateFields(),this.clearStatus(),this.$("button").attr("disabled",null)},newNetwork:function(e){this.model.current_connecting_network=e},networkConnected:function(e){this.model.trigger("connected",m.app.connections.getByConnectionId(e.server)),this.model.current_connecting_network=null},networkDisconnected:function(){this.model.current_connecting_network=null,this.state="all"},networkConnecting:function(){this.model.trigger("connecting"),this.setStatus(m.global.i18n.translate("client_views_serverselect_connection_trying").fetch(),"ok"),this.$(".status").append('<a class="show_server"><i class="fa fa-info-circle"></i></a>')},showServer:function(){this.model.current_connecting_network&&(m.app.view.barsShow(),this.model.current_connecting_network.panels.server.view.show())},onIrcError:function(e){switch($("button",this.$el).attr("disabled",null),e.error){case"nickname_in_use":this.setStatus(m.global.i18n.translate("client_views_serverselect_nickname_error_alreadyinuse").fetch()),this.show("nick_change"),this.$el.find(".nick").select();break;case"erroneus_nickname":this.setStatus(e.reason?e.reason:m.global.i18n.translate("client_views_serverselect_nickname_invalid").fetch()),this.show("nick_change"),this.$el.find(".nick").select();break;case"password_mismatch":this.setStatus(m.global.i18n.translate("client_views_serverselect_password_incorrect").fetch()),this.show("enter_password"),this.$el.find(".password").select();break;default:this.showError(e.reason||"")}},showError:function(e){var t=m.global.i18n.translate("client_views_serverselect_connection_error").fetch();if(e)switch(e){case"ENOTFOUND":t=m.global.i18n.translate("client_views_serverselect_server_notfound").fetch();break;case"ECONNREFUSED":t+=" ("+m.global.i18n.translate("client_views_serverselect_connection_refused").fetch()+")";break;default:t+=" ("+e+")"}this.setStatus(t,"error"),$("button",this.$el).attr("disabled",null),this.show()}}),m.view.StatusMessage=Backbone.View.extend({initialize:function(){this.$el.hide(),this.tmr=null},text:function(e,t){t=t||{},t.type=t.type||"",t.timeout=t.timeout||5e3,this.$el.text(e).addClass(t.type),this.$el.slideDown($.proxy(m.app.view.doLayout,m.app.view)),t.timeout&&this.doTimeout(t.timeout)},html:function(e,t){t=t||{},t.type=t.type||"",t.timeout=t.timeout||5e3,this.$el.html(e).addClass(t.type),this.$el.slideDown($.proxy(m.app.view.doLayout,m.app.view)),t.timeout&&this.doTimeout(t.timeout)},hide:function(){this.$el.slideUp($.proxy(m.app.view.doLayout,m.app.view))},doTimeout:function(e){this.tmr&&clearTimeout(this.tmr);var t=this;this.tmr=setTimeout(function(){t.hide()},e)}}),m.view.Tabs=Backbone.View.extend({tagName:"ul",className:"panellist",events:{"click li":"tabClick","click li .part":"partClick"},initialize:function(){this.model.on("add",this.panelAdded,this),this.model.on("remove",this.panelRemoved,this),this.model.on("reset",this.render,this),this.model.on("active",this.panelActive,this),this.is_network=!1,this.model.network&&(this.is_network=!0,this.model.network.on("change:name",function(e,t){$("span",this.model.server.tab).text(t)},this),this.model.network.on("change:connection_id",function(e,t){this.model.forEach(function(e){e.tab.data("connection_id",t)})},this))},render:function(){var e=this;this.$el.empty(),this.is_network&&this.model.server.tab.data("panel",this.model.server).data("connection_id",this.model.network.get("connection_id")).appendTo(this.$el),this.model.forEach(function(t){this.is_network&&t==e.model.server||(t.tab.data("panel",t),this.is_network&&t.tab.data("connection_id",this.model.network.get("connection_id")),t.tab.appendTo(e.$el))}),m.app.view.doLayout()},updateTabTitle:function(e,t){$("span",e.tab).text(t)},panelAdded:function(e){e.tab=$('<li><span></span><div class="activity"></div></li>'),e.tab.find("span").text(e.get("title")||e.get("name")),e.isServer()&&(e.tab.addClass("server"),e.tab.addClass("fa"),e.tab.addClass("fa-nonexistant")),e.tab.data("panel",e),this.is_network&&e.tab.data("connection_id",this.model.network.get("connection_id")),this.sortTabs(),e.bind("change:title",this.updateTabTitle),e.bind("change:name",this.updateTabTitle),m.app.view.doLayout()},panelRemoved:function(e){m.app.connections.active_connection;e.tab.remove(),delete e.tab,m.app.panels.trigger("remove",e),m.app.view.doLayout()},panelActive:function(e){m.app.view.$el.find(".panellist .part").remove(),m.app.view.$el.find(".panellist .active").removeClass("active"),e.tab.addClass("active"),e.tab.append('<span class="part fa fa-nonexistant"></span>')},tabClick:function(e){var t=$(e.currentTarget),n=t.data("panel");n&&n.view.show()},partClick:function(e){var t=$(e.currentTarget).parent(),n=t.data("panel");n&&(n.isChannel()&&n.get("members").models.length>0?this.model.network.gateway.part(n.get("name")):n.isServer()?(!this.model.network.get("connected")||confirm(d("disconnect_from_server")))&&(this.model.network.gateway.quit("Leaving"),m.app.connections.remove(this.model.network),m.app.startup_applet.view.show()):n.close())},sortTabs:function(){var e=this,t=[];this.model.forEach(function(n){e.is_network&&n==e.model.server||t.push([n.get("title")||n.get("name"),n])}),t.sort(function(e,t){return e[0].toLowerCase()>t[0].toLowerCase()?1:e[0].toLowerCase()<t[0].toLowerCase()?-1:0}),_.each(t,function(t){t[1].tab.appendTo(e.$el)})}}),m.view.TopicBar=Backbone.View.extend({events:{"keydown div":"process"},initialize:function(){m.app.panels.bind("active",function(e){e.isChannel()?(this.setCurrentTopicFromChannel(e),this.$el.find("div").attr("contentEditable",!0)):this.$el.find("div").attr("contentEditable",!1).text("")},this)},process:function(e){var t=$(e.currentTarget),n=t.text();return m.app.panels().active.isChannel()?13===e.keyCode?(m.app.connections.active_connection.gateway.topic(m.app.panels().active.get("name"),n),!1):void 0:!1},setCurrentTopic:function(e){e=e||"",$("div",this.$el).html(l(_.escape(e)))},setCurrentTopicFromChannel:function(e){var t=e.get("topic_set_by"),n="";this.setCurrentTopic(e.get("topic")),t?(n+=d("client_models_network_topic",[t.nick,m.utils.formatDate(t.when)]),this.$el.attr("title",n)):this.$el.attr("title","")}}),m.view.UserBox=Backbone.View.extend({events:{"click .query":"queryClick","click .info":"infoClick","change .ignore":"ignoreChange","click .ignore":"ignoreClick","click .op":"opClick","click .deop":"deopClick","click .voice":"voiceClick","click .devoice":"devoiceClick","click .kick":"kickClick","click .ban":"banClick"},initialize:function(){var e={op:m.global.i18n.translate("client_views_userbox_op").fetch(),de_op:m.global.i18n.translate("client_views_userbox_deop").fetch(),voice:m.global.i18n.translate("client_views_userbox_voice").fetch(),de_voice:m.global.i18n.translate("client_views_userbox_devoice").fetch(),kick:m.global.i18n.translate("client_views_userbox_kick").fetch(),ban:m.global.i18n.translate("client_views_userbox_ban").fetch(),message:m.global.i18n.translate("client_views_userbox_query").fetch(),info:m.global.i18n.translate("client_views_userbox_whois").fetch(),ignore:m.global.i18n.translate("client_views_userbox_ignore").fetch()};this.$el=$(_.template($("#tmpl_userbox").html().trim(),e))},setTargets:function(e,t){this.user=e,this.channel=t;var n=m.app.connections.active_connection.isNickIgnored(this.user.get("nick"));this.$(".ignore input").attr("checked",n?"checked":!1)},displayOpItems:function(e){e?this.$el.find(".if_op").css("display","block"):this.$el.find(".if_op").css("display","none")},queryClick:function(){var e=this.user.get("nick");m.app.connections.active_connection.createQuery(e)},infoClick:function(){m.app.controlbox.processInput("/whois "+this.user.get("nick"))},ignoreClick:function(e){e.stopPropagation()},ignoreChange:function(e){m.app.controlbox.processInput($(e.currentTarget).find("input").is(":checked")?"/ignore "+this.user.get("nick"):"/unignore "+this.user.get("nick"))},opClick:function(){m.app.controlbox.processInput("/mode "+this.channel.get("name")+" +o "+this.user.get("nick"))},deopClick:function(){m.app.controlbox.processInput("/mode "+this.channel.get("name")+" -o "+this.user.get("nick"))},voiceClick:function(){m.app.controlbox.processInput("/mode "+this.channel.get("name")+" +v "+this.user.get("nick"))},devoiceClick:function(){m.app.controlbox.processInput("/mode "+this.channel.get("name")+" -v "+this.user.get("nick"))},kickClick:function(){m.app.controlbox.processInput("/kick "+this.user.get("nick")+" Bye!")},banClick:function(){m.app.controlbox.processInput("/mode "+this.channel.get("name")+" +b "+this.user.get("nick")+"!*")}}),m.view.ChannelTools=Backbone.View.extend({events:{"click .channel_info":"infoClick","click .channel_part":"partClick"},initialize:function(){},infoClick:function(){new m.model.ChannelInfo({channel:m.app.panels().active})},partClick:function(){m.app.connections.active_connection.gateway.part(m.app.panels().active.get("name"))}}),m.view.ChannelInfo=Backbone.View.extend({events:{"click .toggle_banlist":"toggleBanList","change .channel-mode":"onModeChange","click .remove-ban":"onRemoveBanClick"},initialize:function(){var e,t=this.model.get("channel");e={moderated_chat:d("client_views_channelinfo_moderated"),invite_only:d("client_views_channelinfo_inviteonly"),ops_change_topic:d("client_views_channelinfo_opschangechannel"),external_messages:d("client_views_channelinfo_externalmessages"),toggle_banlist:d("client_views_channelinfo_togglebanlist"),channel_name:t.get("name")},this.$el=$(_.template($("#tmpl_channel_info").html().trim(),e)),this.menu=new m.view.MenuBox(t.get("name")),this.menu.addItem("channel_info",this.$el),this.menu.$el.appendTo(t.view.$container),this.menu.show(),this.menu.$el.offset({top:m.app.view.$el.find(".panels").offset().top}),this.$el.dispose=_.bind(this.dispose,this),this.updateInfo(t),t.on("change:info_modes change:info_url change:banlist",this.updateInfo,this),t.get("network").gateway.channelInfo(t.get("name"))},render:function(){},onModeChange:function(e){var t=$(e.currentTarget),n=this.model.get("channel"),i=t.data("mode"),s="";
+return"checkbox"==t.attr("type")?(s=t.is(":checked")?"+":"-",s+=i,void n.setMode(s)):"text"==t.attr("type")?(s=t.val()?"+"+i+" "+t.val():"-"+i,void n.setMode(s)):void 0},onRemoveBanClick:function(e){e.preventDefault(),e.stopPropagation();var t=$(e.currentTarget),n=t.parents("tr:first"),i=n.data("ban");if(i){var s=this.model.get("channel");s.setMode("-b "+i.banned),n.remove()}},updateInfo:function(e){var t,n,i,s=this;if(t=e.get("info_modes"),t&&_.each(t,function(e){e.mode=e.mode.toLowerCase(),"+k"==e.mode?s.$el.find('[name="channel_key"]').val(e.param):"+m"==e.mode?s.$el.find('[name="channel_mute"]').attr("checked","checked"):"+i"==e.mode?s.$el.find('[name="channel_invite"]').attr("checked","checked"):"+n"==e.mode?s.$el.find('[name="channel_external_messages"]').attr("checked","checked"):"+t"==e.mode&&s.$el.find('[name="channel_topic"]').attr("checked","checked")}),n=e.get("info_url"),n&&(this.$el.find(".channel_url").text(n).attr("href",n),this.$el.find(".channel_url").slideDown()),i=e.get("banlist"),i&&i.length){var a=this.$el.find(".channel-banlist table tbody");this.$el.find(".banlist-status").text(""),a.empty(),_.each(i,function(e){var t=$("<tr></tr>").data("ban",e);$("<td></td>").text(e.banned).appendTo(t),$("<td></td>").text(e.banned_by.split(/[!@]/)[0]).appendTo(t),$("<td></td>").text(m.utils.formatDate(new Date(1e3*parseInt(e.banned_at,10)))).appendTo(t),$('<td><i class="fa fa-rtimes remove-ban"></i></td>').appendTo(t),a.append(t)}),this.$el.find(".channel-banlist table").slideDown()}else this.$el.find(".banlist-status").text("Banlist empty"),this.$el.find(".channel-banlist table").hide()},toggleBanList:function(e){if(e.preventDefault(),this.$el.find(".channel-banlist table").toggle(),this.$el.find(".channel-banlist table").is(":visible")){var t=this.model.get("channel"),n=t.get("network");n.gateway.raw("MODE "+t.get("name")+" +b")}},dispose:function(){this.model.get("channel").off("change:info_modes change:info_url change:banlist",this.updateInfo,this),this.$el.remove()}}),m.view.RightBar=Backbone.View.extend({events:{"click .right-bar-toggle":"onClickToggle","click .right-bar-toggle-inner":"onClickToggle"},initialize:function(){this.keep_hidden=!1,this.hidden=this.$el.hasClass("disabled"),this.updateIcon()},hide:function(){this.hidden=!0,this.$el.addClass("disabled"),this.updateIcon()},show:function(){this.hidden=!1,this.keep_hidden||this.$el.removeClass("disabled"),this.updateIcon()},toggle:function(e){return this.ignore_layout?!0:(this.keep_hidden="undefined"==typeof e?!this.keep_hidden:e,this.keep_hidden||this.hidden?this.$el.addClass("disabled"):this.$el.removeClass("disabled"),void this.updateIcon())},updateIcon:function(){var e=this.$(".right-bar-toggle"),t=e.find("i");!this.hidden&&this.keep_hidden?e.show():e.hide(),this.keep_hidden?t.removeClass("fa fa-angle-double-right").addClass("fa fa-users"):t.removeClass("fa fa-users").addClass("fa fa-angle-double-right")},onClickToggle:function(){this.toggle(),this.ignore_layout=!0,m.app.view.doLayout(),delete this.ignore_layout}}),m.view.Notification=Backbone.View.extend({className:"notification",events:{"click .close":"close"},initialize:function(e,t){this.title=e,this.content=t},render:function(){return this.$el.html($("#tmpl_notifications").html()),this.$("h6").text(this.title),"string"==typeof this.content?this.$(".content").html(this.content):"object"==typeof this.content&&this.$(".content").empty().append(this.content),this},show:function(){var e=this;this.render().$el.appendTo(m.app.view.$el),_.defer(function(){e.$el.addClass("show")})},close:function(){this.remove()}}),function(){function e(e,t){this.app=e,this.controlbox=t,this.addDefaultAliases(),this.bindCommand(L)}function n(e){var t=e.command+" "+e.params.join(" ");this.app.connections.active_connection.gateway.raw(t)}function i(){}function s(e){var t,n;n=e.params.join(" ").split(","),t=this.app.connections.active_connection.createAndJoinChannels(n),t.length&&t[t.length-1].view.show()}function a(e){var t,n,i;t=e.params[0],e.params.shift(),n=e.params.join(" "),i=this.app.connections.active_connection.panels.getByName(t),i||(i=new m.model.Query({name:t}),this.app.connections.active_connection.panels.add(i)),i&&i.view.show(),n&&(this.app.connections.active_connection.gateway.msg(i.get("name"),n),i.addMsg(this.app.connections.active_connection.get("nick"),u("privmsg",{text:n}),"privmsg"))}function o(e){var t,n=e.params[0],i=this.app.connections.active_connection.panels.getByName(n)||this.app.panels().server;e.params.shift(),t=e.params.join(" "),i.addMsg(this.app.connections.active_connection.get("nick"),u("privmsg",{text:t}),"privmsg"),this.app.connections.active_connection.gateway.msg(n,t)}function c(e){if(!this.app.panels().active.isServer()){var t=this.app.panels().active;t.addMsg("",u("action",{nick:this.app.connections.active_connection.get("nick"),text:e.params.join(" ")}),"action"),this.app.connections.active_connection.gateway.action(t.get("name"),e.params.join(" "))}}function l(e){var t,n,i=this;0===e.params.length?this.app.connections.active_connection.gateway.part(this.app.panels().active.get("name")):(t=e.params[0].split(","),n=e.params[1],_.each(t,function(e){i.connections.active_connection.gateway.part(e,n)}))}function r(e){var t,n=this;t=0===e.params.length?this.app.panels().active.get("name"):e.params[0],this.app.connections.active_connection.gateway.part(t),setTimeout(function(){n.app.connections.active_connection.createAndJoinChannels(t),n.app.connections.active_connection.panels.getByName(t).show()},1e3)}function h(e){this.app.connections.active_connection.gateway.changeNick(e.params[0])}function p(e){var t;0!==e.params.length&&(this.app.connections.active_connection.isChannelName(e.params[0])?(t=e.params[0],e.params.shift()):t=this.app.panels().active.get("name"),this.app.connections.active_connection.gateway.topic(t,e.params.join(" ")))}function g(e){var t;e.params.length<=1||(t=e.params[0],e.params.shift(),this.app.connections.active_connection.gateway.notice(t,e.params.join(" ")))}function f(e){var t=e.params.join(" ");this.app.connections.active_connection.gateway.raw(t)}function v(e){var t,n=this.app.panels().active;n.isChannel()&&0!==e.params.length&&(t=e.params[0],e.params.shift(),this.app.connections.active_connection.gateway.kick(n.get("name"),t,e.params.join(" ")))}function w(){this.app.panels().active.isServer()||this.app.panels().active.isApplet()||this.app.panels().active.clearMessages&&this.app.panels().active.clearMessages()}function k(e){var t,n;e.params.length<2||(t=e.params[0],e.params.shift(),n=e.params[0],e.params.shift(),this.app.connections.active_connection.gateway.ctcpRequest(n,t,e.params.join(" ")))}function b(){var e=m.model.Applet.loadOnce("kiwi_settings");e.view.show()}function y(){var e=m.model.Applet.loadOnce("kiwi_script_editor");e.view.show()}function C(e){if(e.params[0]){var t=new m.model.Applet;if(e.params[1])t.load(e.params[0],e.params[1]);else{if(!this.applets[e.params[0]])return void this.app.panels().server.addMsg("",u("applet_notfound",{text:d("client_models_application_applet_notfound",[e.params[0]])}));t.load(new this.applets[e.params[0]])}this.app.connections.active_connection.panels.add(t),t.view.show()}}function x(e){var t,n;e.params[0]&&this.app.panels().active.isChannel()&&(t=e.params[0],n=this.app.panels().active.get("name"),this.app.connections.active_connection.gateway.raw("INVITE "+t+" "+n),this.app.panels().active.addMsg("",u("channel_has_been_invited",{nick:t,text:d("client_models_application_has_been_invited",[n])}),"action"))}function M(e){var t;e.params[0]?t=e.params[0]:this.app.panels().active.isQuery()&&(t=this.app.panels().active.get("name")),t&&this.app.connections.active_connection.gateway.raw("WHOIS "+t+" "+t)}function S(e){var t;e.params[0]?t=e.params[0]:this.app.panels().active.isQuery()&&(t=this.app.panels().active.get("name")),t&&this.app.connections.active_connection.gateway.raw("WHOWAS "+t)}function T(e){this.app.connections.active_connection.gateway.raw("AWAY :"+e.params.join(" "))}function N(e){var t=this;e.params[0]?m.gateway.setEncoding(null,e.params[0],function(n){n?t.app.panels().active.addMsg("",u("encoding_changed",{text:d("client_models_application_encoding_changed",[e.params[0]])})):t.app.panels().active.addMsg("",u("encoding_invalid",{text:d("client_models_application_encoding_invalid",[e.params[0]])}))}):(this.app.panels().active.addMsg("",u("client_models_application_encoding_notspecified",{text:d("client_models_application_encoding_notspecified")})),this.app.panels().active.addMsg("",u("client_models_application_encoding_usage",{text:d("client_models_application_encoding_usage")})))}function B(){var e=this.app.panels().active;e.isChannel()&&new m.model.ChannelInfo({channel:this.app.panels().active})}function D(e){var t=this.app.connections.active_connection;t&&t.gateway.quit(e.params.join(" "))}function I(e){var n,i,s,a,o,c,l=this;return e.params[0]?(e.params[0].indexOf(":")>0?(c=e.params[0].split(":"),n=c[0],i=c[1],a=e.params[1]||t):(n=e.params[0],i=e.params[1]||6667,a=e.params[2]||t),"+"===i.toString()[0]?(s=!0,i=parseInt(i.substring(1),10)):s=!1,i=i||6667,o=this.app.connections.active_connection.get("nick"),this.app.panels().active.addMsg("",u("server_connecting",{text:d("client_models_application_connection_connecting",[n,i.toString()])})),void m.gateway.newConnection({nick:o,host:n,port:i,ssl:s,password:a},function(e){var t;e&&(t=d("client_models_application_connection_error",[n,i.toString(),e.toString()]),l.app.panels().active.addMsg("",u("server_connecting_error",{text:t})))})):(c=new m.view.MenuBox(m.global.i18n.translate("client_models_application_connection_create").fetch()),c.addItem("new_connection",(new m.model.NewConnection).view.$el),c.show(),void c.$el.offset({top:this.app.view.$el.height()/2-c.$el.height()/2,left:this.app.view.$el.width()/2-c.$el.width()/2}))}m.misc.ClientUiCommands=e,e.prototype.addDefaultAliases=function(){$.extend(this.controlbox.preprocessor.aliases,{"/p":"/part $1+","/me":"/action $1+","/j":"/join $1+","/q":"/query $1+","/w":"/whois $1+","/raw":"/quote $1+","/connect":"/server $1+","/op":"/quote mode $channel +o $1+","/deop":"/quote mode $channel -o $1+","/hop":"/quote mode $channel +h $1+","/dehop":"/quote mode $channel -h $1+","/voice":"/quote mode $channel +v $1+","/devoice":"/quote mode $channel -v $1+","/k":"/kick $channel $1+","/ban":"/quote mode $channel +b $1+","/unban":"/quote mode $channel -b $1+","/slap":"/me slaps $1 around a bit with a large trout","/tick":"/msg $channel âœ”"})},e.prototype.bindCommand=function(e){var t=this;_.each(e,function(e,n){t.controlbox.on(n,_.bind(e,t))})};var L={unknown_command:n,command:i,"command:msg":o,"command:action":c,"command:join":s,"command:part":l,"command:cycle":r,"command:nick":h,"command:query":a,"command:invite":x,"command:topic":p,"command:notice":g,"command:quote":f,"command:kick":v,"command:clear":w,"command:ctcp":k,"command:quit":D,"command:server":I,"command:whois":M,"command:whowas":S,"command:away":T,"command:encoding":N,"command:channel":B,"command:applet":C,"command:settings":b,"command:script":y};L["command:css"]=function(){var e="?reload="+(new Date).getTime();$('link[rel="stylesheet"]').each(function(){this.href=this.href.replace(/\?.*|$/,e)})},L["command:js"]=function(e){e.params[0]&&$script(e.params[0]+"?"+(new Date).getTime())},L["command:set"]=function(e){if(e.params[0]){var t,n=e.params[0];e.params[1]&&(e.params.shift(),t=e.params.join(" "),"true"===t&&(t=!0),"false"===t&&(t=!1),parseInt(t,10).toString()===t&&(t=parseInt(t,10)),m.global.settings.set(n,t)),this.app.panels().active.addMsg("",u("set_setting",{text:n+" = "+m.global.settings.get(n).toString()}))}},L["command:save"]=function(){m.global.settings.save(),this.app.panels().active.addMsg("",u("settings_saved",{text:d("client_models_application_settings_saved")}))},L["command:alias"]=function(e){var t,n,i=this;return e.params[1]?"del"===e.params[0]||"delete"===e.params[0]?(t=e.params[1],"/"!==t[0]&&(t="/"+t),void delete this.controlbox.preprocessor.aliases[t]):(t=e.params[0],e.params.shift(),n=e.params.join(" "),"/"!==t[0]&&(t="/"+t),void(this.controlbox.preprocessor.aliases[t]=n)):void $.each(this.controlbox.preprocessor.aliases,function(e,t){i.app.panels().server.addMsg(" ",u("list_aliases",{text:e+"   =>   "+t}))})},L["command:ignore"]=function(e){var t=this,n=this.app.connections.active_connection.get("ignore_list");return e.params[0]?(n.push(e.params[0]),this.app.connections.active_connection.set("ignore_list",n),void this.app.panels().active.addMsg(" ",u("ignore_nick",{text:d("client_models_application_ignore_nick",[e.params[0]])}))):void(n.length>0?(this.app.panels().active.addMsg(" ",u("ignore_title",{text:d("client_models_application_ignore_title")})),$.each(n,function(e,n){t.app.panels().active.addMsg(" ",u("ignored_pattern",{text:n}))})):this.app.panels().active.addMsg(" ",u("ignore_none",{text:d("client_models_application_ignore_none")})))},L["command:unignore"]=function(e){var t=this.app.connections.active_connection.get("ignore_list");return e.params[0]?(t=_.reject(t,function(t){return t===e.params[0]}),this.app.connections.active_connection.set("ignore_list",t),void this.app.panels().active.addMsg(" ",u("ignore_stopped",{text:d("client_models_application_ignore_stopped",[e.params[0]])}))):void this.app.panels().active.addMsg(" ",u("ignore_stop_notice",{text:d("client_models_application_ignore_stop_notice")}))}}(),function(){var e=Backbone.View.extend({events:{"change [data-setting]":"saveSettings",'click [data-setting="theme"]':"selectTheme","click .register_protocol":"registerProtocol","click .enable_notifications":"enableNotifications"},initialize:function(){var e={tabs:d("client_applets_settings_channelview_tabs"),list:d("client_applets_settings_channelview_list"),large_amounts_of_chans:d("client_applets_settings_channelview_list_notice"),join_part:d("client_applets_settings_notification_joinpart"),count_all_activity:d("client_applets_settings_notification_count_all_activity"),timestamps:d("client_applets_settings_timestamp"),timestamp_24:d("client_applets_settings_timestamp_24_hour"),mute:d("client_applets_settings_notification_sound"),emoticons:d("client_applets_settings_emoticons"),scroll_history:d("client_applets_settings_history_length"),languages:m.app.translations,default_client:d("client_applets_settings_default_client"),make_default:d("client_applets_settings_default_client_enable"),locale_restart_needed:d("client_applets_settings_locale_restart_needed"),default_note:d("client_applets_settings_default_client_notice",'<a href="chrome://settings/handlers">chrome://settings/handlers</a>'),html5_notifications:d("client_applets_settings_html5_notifications"),enable_notifications:d("client_applets_settings_enable_notifications"),theme_thumbnails:_.map(m.app.themes,function(e){return _.template($("#tmpl_theme_thumbnail").html().trim(),e)})};this.$el=$(_.template($("#tmpl_applet_settings").html().trim(),e)),navigator.registerProtocolHandler||this.$(".protocol_handler").remove(),null!==m.utils.notifications.allowed()&&this.$(".notification_enabler").remove(),m.global.settings.on("change",this.loadSettings,this),this.loadSettings()},loadSettings:function(){_.each(m.global.settings.attributes,function(e,t){var n=this.$('[data-setting="'+t+'"]');if(n.length)switch(n.prop("type")){case"checkbox":n.prop("checked",e);break;case"radio":this.$('[data-setting="'+t+'"][value="'+e+'"]').prop("checked",!0);break;case"text":n.val(e);break;case"select-one":this.$('[value="'+e+'"]').prop("selected",!0);break;default:this.$('[data-setting="'+t+'"][data-value="'+e+'"]').addClass("active")}},this)},saveSettings:function(e){var t,n=m.global.settings,i=$(e.currentTarget);switch(e.currentTarget.type){case"checkbox":t=i.is(":checked");break;case"radio":case"text":t=i.val();break;case"select-one":t=$(e.currentTarget[i.prop("selectedIndex")]).val();break;default:t=i.data("value")}m.global.settings.off("change",this.loadSettings,this),n.set(i.data("setting"),t),n.save(),m.global.settings.on("change",this.loadSettings,this)},selectTheme:function(e){e.preventDefault(),this.$('[data-setting="theme"].active').removeClass("active"),$(e.currentTarget).addClass("active").trigger("change")},registerProtocol:function(e){e.preventDefault(),navigator.registerProtocolHandler("irc",document.location.origin+m.app.get("base_path")+"/%s","Kiwi IRC"),navigator.registerProtocolHandler("ircs",document.location.origin+m.app.get("base_path")+"/%s","Kiwi IRC")},enableNotifications:function(e){e.preventDefault();var t=m.utils.notifications;t.requestPermission().always(_.bind(function(){null!==t.allowed()&&this.$(".notification_enabler").remove()},this))}}),t=Backbone.Model.extend({initialize:function(){this.set("title",d("client_applets_settings_title")),this.view=new e}});m.model.Applet.register("kiwi_settings",t)}(),function(){var e=Backbone.View.extend({events:{"click .chan":"chanClick","click .channel_name_title":"sortChannelsByNameClick","click .users_title":"sortChannelsByUsersClick"},initialize:function(){var e={channel_name:m.global.i18n.translate("client_applets_chanlist_channelname").fetch(),users:m.global.i18n.translate("client_applets_chanlist_users").fetch(),topic:m.global.i18n.translate("client_applets_chanlist_topic").fetch()};this.$el=$(_.template($("#tmpl_channel_list").html().trim(),e)),this.channels=[],this.order="",this.waiting=!1},render:function(){var e,t=$("table",this.$el),n=t.children("tbody:first").detach();switch(0==$(".applet_chanlist .users_title").find("span.chanlist_sort_users").length?this.$(".users_title").append('<span class="chanlist_sort_users">&nbsp;&nbsp;</span>'):(this.$(".users_title span.chanlist_sort_users").removeClass("fa fa-sort-desc"),this.$(".users_title span.chanlist_sort_users").removeClass("fa fa-sort-asc")),0==$(".applet_chanlist .channel_name_title").find("span.chanlist_sort_names").length?this.$(".channel_name_title").append('<span class="chanlist_sort_names">&nbsp;&nbsp;</span>'):(this.$(".channel_name_title span.chanlist_sort_names").removeClass("fa fa-sort-desc"),this.$(".channel_name_title span.chanlist_sort_names").removeClass("fa fa-sort-asc")),this.order){case"user_desc":default:this.$(".users_title span.chanlist_sort_users").addClass("fa fa-sort-asc");break;case"user_asc":this.$(".users_title span.chanlist_sort_users").addClass("fa fa-sort-desc");break;case"name_asc":this.$(".channel_name_title span.chanlist_sort_names").addClass("fa fa-sort-desc");break;case"name_desc":this.$(".channel_name_title span.chanlist_sort_names").addClass("fa fa-sort-asc")}for(this.channels=this.sortChannels(this.channels,this.order),e=0;e<this.channels.length;e++)n[0].appendChild(this.channels[e].dom);t[0].appendChild(n[0])},chanClick:function(e){e.target?m.gateway.join(null,$(e.target).data("channel")):m.gateway.join(null,$(e.srcElement).data("channel"))},sortChannelsByNameClick:function(){this.order="name_asc"==this.order?"name_desc":"name_asc",this.sortChannelsClick()},sortChannelsByUsersClick:function(){this.order="user_desc"==this.order||""==this.order?"user_asc":"user_desc",this.sortChannelsClick()},sortChannelsClick:function(){this.render()},sortChannels:function(e,t){var n=[],i=[];return _.each(e,function(e,t){n.push({chan_idx:t,num_users:e.num_users,channel:e.channel})}),n.sort(function(e,n){switch(t){case"user_asc":return e.num_users-n.num_users;case"user_desc":return n.num_users-e.num_users;case"name_asc":if(e.channel.toLowerCase()>n.channel.toLowerCase())return 1;if(e.channel.toLowerCase()<n.channel.toLowerCase())return-1;case"name_desc":if(e.channel.toLowerCase()<n.channel.toLowerCase())return 1;if(e.channel.toLowerCase()>n.channel.toLowerCase())return-1;default:return n.num_users-e.num_users}return 0}),_.each(n,function(t){i.push(e[t.chan_idx])}),i}}),t=Backbone.Model.extend({initialize:function(){this.set("title",m.global.i18n.translate("client_applets_chanlist_channellist").fetch()),this.view=new e,this.network=m.global.components.Network(),this.network.on("list_channel",this.onListChannel,this),this.network.on("list_start",this.onListStart,this)},onListChannel:function(e){this.addChannel(e.chans)},onListStart:function(){},addChannel:function(e){var t=this;_.isArray(e)||(e=[e]),_.each(e,function(e){var n;n=document.createElement("tr"),n.innerHTML='<td class="chanlist_name"><a class="chan" data-channel="'+e.channel+'">'+_.escape(e.channel)+'</a></td><td class="chanlist_num_users" style="text-align: center;">'+e.num_users+'</td><td style="padding-left: 2em;" class="chanlist_topic">'+l(_.escape(e.topic))+"</td>",e.dom=n,t.view.channels.push(e)}),t.view.waiting||(t.view.waiting=!0,_.defer(function(){t.view.render(),t.view.waiting=!1}))},dispose:function(){this.view.channels=null,this.view.unbind(),this.view.$el.html(""),this.view.remove(),this.view=null,this.network.off()}});m.model.Applet.register("kiwi_chanlist",t)}(),function(){var e=Backbone.View.extend({events:{"click .btn_save":"onSave"},initialize:function(){var e=this,t={save:m.global.i18n.translate("client_applets_scripteditor_save").fetch()};this.$el=$(_.template($("#tmpl_script_editor").html().trim(),t)),this.model.on("applet_loaded",function(){e.$el.parent().css("height","100%"),$script(m.app.get("base_path")+"/assets/libs/ace/ace.js",function(){e.createAce()})})},createAce:function(){var e="editor_"+Math.floor(1e7*Math.random()).toString();this.editor_id=e,this.$el.find(".editor").attr("id",e),this.editor=ace.edit(e),this.editor.setTheme("ace/theme/monokai"),this.editor.getSession().setMode("ace/mode/javascript");var t=m.global.settings.get("user_script")||"";this.editor.setValue(t)},onSave:function(){var e,t;e="var network = kiwi.components.Network();\n",e+="var input = kiwi.components.ControlInput();\n",e+="var events = kiwi.components.Events();\n",e+=this.editor.getValue()+"\n",e+="this._dispose = function(){ network.off(); input.off(); events.dispose(); if(this.dispose) this.dispose(); }";try{t=new Function(e),m.user_script&&m.user_script._dispose&&m.user_script._dispose(),m.user_script=new t}catch(n){return void this.setStatus(m.global.i18n.translate("client_applets_scripteditor_error").fetch(n.toString()))}m.global.settings.set("user_script",this.editor.getValue()),m.global.settings.save(),this.setStatus(m.global.i18n.translate("client_applets_scripteditor_saved").fetch()+" :)")},setStatus:function(e){var t=this.$el.find(".toolbar .status");e=e||"",t.slideUp("fast",function(){t.text(e),t.slideDown()})}}),t=Backbone.Model.extend({initialize:function(){this.set("title",m.global.i18n.translate("client_applets_scripteditor_title").fetch()),this.view=new e({model:this})}});m.model.Applet.register("kiwi_script_editor",t)}(),function(){var e=Backbone.View.extend({events:{},initialize:function(){this.showConnectionDialog()},showConnectionDialog:function(){var e=this.connection_dialog=new m.model.NewConnection;e.populateDefaultServerSettings(),e.view.$el.addClass("initial"),this.$el.append(e.view.$el);var t=$($("#tmpl_new_connection_info").html().trim());t.html()?e.view.infoBoxSet(t):t=null,this.listenTo(e,"connected",this.newConnectionConnected),_.defer(function(){t&&e.view.infoBoxShow(),window==window.top&&e.view.$el.find(".nick").select()})},newConnectionConnected:function(){this.connection_dialog.view.reset()}}),t=Backbone.Model.extend({initialize:function(){this.view=new e({model:this})}});m.model.Applet.register("kiwi_startup",t)}(),m.utils.notifications=function(){function e(e,n){t.call(this,e,n)}function t(e,i){switch(n.allowed()){case!0:this.notification=new Notification(e,i),_.each(["click","close","error","show"],function(e){this.notification["on"+e]=_.bind(this.trigger,this,e)},this);break;case null:n.requestPermission().done(_.bind(t,this,e,i))}}if(!window.Notification)return{allowed:_.constant(!1),requestPermission:_.constant($.Deferred().reject())};var n={allowed:function(){return"granted"===Notification.permission?!0:"denied"===Notification.permission?!1:null},requestPermission:function(){var e=$.Deferred();return Notification.requestPermission(function(t){e["granted"===t?"resolve":"reject"]()}),e.promise()},create:function(t,n){return new e(t,n)}};return _.extend(e.prototype,Backbone.Events,{closed:!1,_closeTimeout:null,closeAfter:function(e){return this.closed||(this.notification?this._closeTimeout=this._closeTimeout||setTimeout(_.bind(this.close,this),e):this.once("show",_.bind(this.closeAfter,this,e))),this},close:function(){return this.notification&&!this.closed&&(this.notification.close(),this.closed=!0),this}}),n}(),m.utils.formatDate=function(){var e,t,n,i,s=!1,a={d:function(){return(this.getDate()<10?"0":"")+this.getDate()},D:function(){return Date.shortDays[this.getDay()]},j:function(){return this.getDate()},l:function(){return Date.longDays[this.getDay()]},N:function(){return this.getDay()+1},S:function(){return this.getDate()%10==1&&11!=this.getDate()?"st":this.getDate()%10==2&&12!=this.getDate()?"nd":this.getDate()%10==3&&13!=this.getDate()?"rd":"th"},w:function(){return this.getDay()},z:function(){var e=new Date(this.getFullYear(),0,1);return Math.ceil((this-e)/864e5)},W:function(){var e=new Date(this.getFullYear(),0,1);return Math.ceil(((this-e)/864e5+e.getDay()+1)/7)},F:function(){return Date.longMonths[this.getMonth()]},m:function(){return(this.getMonth()<9?"0":"")+(this.getMonth()+1)},M:function(){return Date.shortMonths[this.getMonth()]},n:function(){return this.getMonth()+1},t:function(){var e=new Date;return new Date(e.getFullYear(),e.getMonth(),0).getDate()},L:function(){var e=this.getFullYear();return e%400==0||e%100!=0&&e%4==0},o:function(){var e=new Date(this.valueOf());return e.setDate(e.getDate()-(this.getDay()+6)%7+3),e.getFullYear()},Y:function(){return this.getFullYear()},y:function(){return(""+this.getFullYear()).substr(2)},a:function(){return this.getHours()<12?"am":"pm"},A:function(){return this.getHours()<12?"AM":"PM"},B:function(){return Math.floor(1e3*((this.getUTCHours()+1)%24+this.getUTCMinutes()/60+this.getUTCSeconds()/3600)/24)},g:function(){return this.getHours()%12||12},G:function(){return this.getHours()},h:function(){return((this.getHours()%12||12)<10?"0":"")+(this.getHours()%12||12)},H:function(){return(this.getHours()<10?"0":"")+this.getHours()},i:function(){return(this.getMinutes()<10?"0":"")+this.getMinutes()},s:function(){return(this.getSeconds()<10?"0":"")+this.getSeconds()},u:function(){var e=this.getMilliseconds();return(10>e?"00":100>e?"0":"")+e},e:function(){return"Not Yet Supported"},I:function(){for(var e=null,t=0;12>t;++t){var n=new Date(this.getFullYear(),t,1),i=n.getTimezoneOffset();if(null===e)e=i;else{if(e>i){e=i;break}if(i>e)break}}return this.getTimezoneOffset()==e|0},O:function(){return(-this.getTimezoneOffset()<0?"-":"+")+(Math.abs(this.getTimezoneOffset()/60)<10?"0":"")+Math.abs(this.getTimezoneOffset()/60)+"00"},P:function(){return(-this.getTimezoneOffset()<0?"-":"+")+(Math.abs(this.getTimezoneOffset()/60)<10?"0":"")+Math.abs(this.getTimezoneOffset()/60)+":00"},T:function(){var e=this.getMonth();this.setMonth(0);var t=this.toTimeString().replace(/^.+ \(?([^\)]+)\)?$/,"$1");return this.setMonth(e),t},Z:function(){return 60*-this.getTimezoneOffset()},c:function(){return this.format("Y-m-d\\TH:i:sP")},r:function(){return this.toString()},U:function(){return this.getTime()/1e3}},o=function(){e=[m.global.i18n.translate("client.libs.date_format.short_months.january").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.february").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.march").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.april").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.may").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.june").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.july").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.august").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.september").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.october").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.november").fetch(),m.global.i18n.translate("client.libs.date_format.short_months.december").fetch()],t=[m.global.i18n.translate("client.libs.date_format.long_months.january").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.february").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.march").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.april").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.may").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.june").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.july").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.august").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.september").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.october").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.november").fetch(),m.global.i18n.translate("client.libs.date_format.long_months.december").fetch()],n=[m.global.i18n.translate("client.libs.date_format.short_days.monday").fetch(),m.global.i18n.translate("client.libs.date_format.short_days.tuesday").fetch(),m.global.i18n.translate("client.libs.date_format.short_days.wednesday").fetch(),m.global.i18n.translate("client.libs.date_format.short_days.thursday").fetch(),m.global.i18n.translate("client.libs.date_format.short_days.friday").fetch(),m.global.i18n.translate("client.libs.date_format.short_days.saturday").fetch(),m.global.i18n.translate("client.libs.date_format.short_days.sunday").fetch()],i=[m.global.i18n.translate("client.libs.date_format.long_days.monday").fetch(),m.global.i18n.translate("client.libs.date_format.long_days.tuesday").fetch(),m.global.i18n.translate("client.libs.date_format.long_days.wednesday").fetch(),m.global.i18n.translate("client.libs.date_format.long_days.thursday").fetch(),m.global.i18n.translate("client.libs.date_format.long_days.friday").fetch(),m.global.i18n.translate("client.libs.date_format.long_days.saturday").fetch(),m.global.i18n.translate("client.libs.date_format.long_days.sunday").fetch()],s=!0};return function(e,t){return s||o(),e=e||new Date,t=t||m.global.i18n.translate("client_date_format").fetch(),t.replace(/(\\?)(.)/g,function(t,n,i){return""===n&&a[i]?a[i].call(e):i})}}(),n.prototype.on=function(e,t,n){this._listeners[e]=this._listeners[e]||[],this._listeners[e].push(["on",t,n])},n.prototype.once=function(e,t,n){this._listeners[e]=this._listeners[e]||[],this._listeners[e].push(["once",t,n])},n.prototype.off=function(e,t,n){var i;if("undefined"==typeof e)this._listeners={};else if("undefined"==typeof t)delete this._listeners[e];else if("undefined"==typeof n)for(i in this._listeners[e]||[])this._listeners[e][i][1]===t&&delete this._listeners[e][i];else for(i in this._listeners[e]||[])this._listeners[e][i][1]===t&&this._listeners[e][i][2]===n&&delete this._listeners[e][i]},n.prototype.getListeners=function(e){return this._listeners[e]||[]},n.prototype.createProxy=function(){var e=new n;return e._parent=this._parent||this,e._parent._children.push(e),e},n.prototype.dispose=function(){if(this.off(),this._parent){var e=this._parent._children.indexOf(this);e>-1&&this._parent._children.splice(e,1)}},n.prototype.emit=function(e,n){var i,s=new this.EmitCall(e,n),a=[];for(i=this._children.length-1;i>=0;i--)a=a.concat(this._children[i].getListeners(e));return a=a.concat(this.getListeners(e)),s.then(function(){var e,n=a.length;for(e=0;n>e;e++)"once"===a[e][0]&&(a[e]=t)}),s.callListeners(a),s},n.prototype.EmitCall=function(e,n){function i(e){function i(){var o,l;return a++,e[a]?(l={wait:!1,callback:function(){l.callback=t,i.apply(c)},preventDefault:function(){h=!0}},o=e[a],o[1].call(o[2]||c,l,n),void(l.wait||(l.callback=t,i()))):void s()
+}var a=-1;return n=n||t,0===e.length?void s():void i()}function s(){l=!0;var e=h?p:r;e=e||[];for(var t=0;t<e.length;t++)"function"==typeof e[t]&&e[t]()}function a(e){return"function"!=typeof e?!1:(r.push(e),l&&!h&&e(),this)}function o(e){return"function"!=typeof e?!1:(p.push(e),l&&h&&e(),this)}var c=this,l=!1,r=[],h=!1,p=[];return{callListeners:i,then:a,"catch":o}},"object"==typeof module&&"undefined"!=typeof module.exports&&(module.exports=n),"undefined"==typeof String.prototype.trim&&(String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")}),"undefined"==typeof String.prototype.lpad&&(String.prototype.lpad=function(e,t){var n,i="";for(n=0;e>n;n++)i+=t;return(i+this).slice(-e)})}(window);
\ No newline at end of file
diff --git a/client/index.html~ b/client/index.html~
new file mode 100644 (file)
index 0000000..7dfb542
--- /dev/null
@@ -0,0 +1,611 @@
+<!DOCTYPE html>\r
+<html>\r
+<head>\r
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>\r
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">\r
+<base target="_blank">\r
+\r
+<title> Kiwi IRC2 </title>\r
+\r
+<link rel="shortcut icon" href="/kiwi/assets/img/favicon.ico">\r
+\r
+<link rel="stylesheet" type="text/css" href="/kiwi/assets/css/style.css?t=1458242884965" />\r
+<link rel="stylesheet" type="text/css" href="/kiwi/assets/css/font-awesome.min.css" />\r
+</head>\r
+<body>\r
+  <a href="./kiwi/weblabels.html" rel="jslicense">licensing information</a> \r
+    \r
+    <script type="text/html" id="tmpl_application">\r
+        <div id="kiwi" class="theme_relaxed">\r
+            <div class="toolbar">\r
+                <div class="app_tools">\r
+                    <ul class="main">\r
+                        <li class="settings"><i class="fa fa-cogs" title="Settings"></i></li>\r
+                        <li class="startup"><i class="fa fa-home" title="Home"></i></li>\r
+                        <li><a href="https://kiwiirc.com/" target="_blank"><img src="/kiwi/assets/img/ico.png" alt="KiwiIRC" title="KiwiIRC" /></a></li>\r
+                    </ul>\r
+                </div>\r
+\r
+                <div class="tabs"></div>\r
+\r
+                <div class="topic">\r
+                    <div contenteditable="true"></div>\r
+                </div>\r
+\r
+                <div class="status_message"></div>\r
+            </div>\r
+\r
+            <div class="memberlists_resize_handle"></div>\r
+\r
+            <div class="panels">\r
+                <div class="panel_container container1"></div>\r
+            </div>\r
+\r
+            <div class="right_bar disabled">\r
+                <div class="right-bar-toggle"><i class=""></i></div>\r
+                <div class="right-bar-content">\r
+                    <div class="channel_tools">\r
+                        <i class="fa fa-info-circle channel_info" title="Channel Info"></i>\r
+                        <i class="fa fa-sign-out channel_part" title="Leave Channel"></i>\r
+                        <i class="fa fa-angle-double-right right-bar-toggle-inner" title="Hide"></i>\r
+                    </div>\r
+                    <div class="memberlists"></div>\r
+                </div>\r
+            </div>\r
+\r
+            <div class="controlbox">\r
+                <div class="input">\r
+                    <span class="nick"> </span>\r
+                    <div class="input_wrap"><textarea class="inp"></textarea></div>\r
+                    <div class="input_tools"></div>\r
+                </div>\r
+            </div>\r
+        </div>\r
+    </script>\r
+\r
+\r
+    <script type="text/html" id="tmpl_channel_info">\r
+        <div class="channel_info">\r
+            <b class="channel_url"><a href=""></a></b>\r
+\r
+            <form>\r
+                <div class="control-group channel_info_modes">\r
+                    <label>\r
+                        <input type="checkbox" name="channel_mute" class="channel-mode" data-mode="m" />\r
+                        Moderated chat\r
+                    </label>\r
+                    <label>\r
+                        <input type="checkbox" name="channel_invite" class="channel-mode" data-mode="i" />\r
+                        Invite only\r
+                    </label>\r
+                    <label>\r
+                        <input type="checkbox" name="channel_topic" class="channel-mode" data-mode="t" />\r
+                        Only operators can change the topic\r
+                    </label>\r
+                    <label>\r
+                        <input type="checkbox" name="channel_external_messages" class="channel-mode" data-mode="n" />\r
+                        Block messages from outside this channel\r
+                    </label>\r
+                </div>\r
+\r
+                <div class="control-group">\r
+                    <label>\r
+                        Password\r
+                        <input type="text" name="channel_key" class="channel-mode" data-mode="k" />\r
+                    </label>\r
+                </div>\r
+\r
+\r
+                <div class="control-group channel-banlist">\r
+                    <button class="toggle_banlist">Toggle banlist</button> <span class="banlist-status"></span>\r
+                    <table>\r
+                        <thead>\r
+                            <tr>\r
+                                <td>Ban Mask</td>\r
+                                <td>Added By</td>\r
+                                <td>Date Added</td>\r
+                                <td></td>\r
+                            </tr>\r
+                        </thead>\r
+                        <tbody>\r
+                        </tbody>\r
+                    </table>\r
+                </div>\r
+            </form>\r
+        </div>\r
+    </script>\r
+\r
+\r
+    <script type="text/html" id="tmpl_userbox">\r
+        <div class="userbox">\r
+            <a class="close_menu if_op op"><i class="fa fa-star"></i><%= op %></a>\r
+            <a class="close_menu if_op deop"><i class="fa fa-star-o"></i><%= de_op %></a>\r
+            <a class="close_menu if_op voice"><i class="fa fa-volume-up"></i><%= voice %></a>\r
+            <a class="close_menu if_op devoice"><i class="fa fa-volume-off"></i><%= de_voice %></a>\r
+            <a class="close_menu if_op kick"><i class="fa fa-times"></i><%= kick %></a>\r
+            <a class="close_menu if_op ban"><i class="fa fa-ban"></i><%= ban %></a>\r
+\r
+            <a class="close_menu query"><i class="fa fa-comment"></i><%= message %></a>\r
+            <a class="close_menu info"><i class="fa fa-info-circle"></i><%= info %></a>\r
+            <a class="close_menu ignore"><label><i><input type="checkbox" /></i><%= ignore %></label></a>\r
+        </div>\r
+    </script>\r
+\r
+    <script type="text/html" id="tmpl_nickchange">\r
+        <form class="nickchange">\r
+            <label for="nickchange"><%= new_nick %>:</label> <input type="text" mozactionhint="done" autocomplete="off" spellcheck="false"/> <button><%= change %></button> <a class="cancel"><%= cancel %></a>\r
+        </form>\r
+    </script>\r
+\r
+    <script type="text/html" id="tmpl_new_connection_info">\r
+        <div style="margin:1em 20px;">\r
+            <img src="/kiwi/assets/img/ico.png" alt="KiwiIRC Logo" title="Kiwi IRC" style="display:block; margin:0 auto;"/> <br />\r
+            <p style="font-style:italic;">A <strong>hand-crafted IRC client</strong> that you can enjoy. Designed to be used <strong>easily</strong> and <strong>freely</strong>.</p>\r
+\r
+            <p style="font-size:0.9em;margin-top:2em;">Peek at the <a href="https://www.kiwiirc.com/">Kiwi IRC homepage</a> for more information or to find out how to embed it on your own website. Looking for source code? Try the <a href="http://github.com/prawnsalad/KiwiIRC/">GitHub</a> page. This network of people may not be associated with Kiwi IRC itself.</p>\r
+        </div>\r
+    </script>\r
+\r
+    <script type="text/html" id="tmpl_server_select">\r
+        <div class="server_select">\r
+\r
+            <div class="side_panel" style="position:absolute;top:0px;left:320px;">\r
+                <div class="content" style="position:relative;width:300px;">\r
+                </div>\r
+            </div>\r
+\r
+            <div class="server_details" style="position:relative;width:320px;">\r
+                <div class="status"><%= think_nick %></div>\r
+\r
+                <form>\r
+                    <div class="basic">\r
+                        <table>\r
+                            <tr class="nick">\r
+                                <td><label for="server_select_nick"><%= nickname %></label></td>\r
+                                <td><input type="text" class="nick" id="server_select_nick"></td>\r
+                            </tr>\r
+\r
+                            <tr class="have_pass">\r
+                                <td colspan="2">\r
+                                    <label for="server_select_show_pass"><%= have_password %></label> <input type="checkbox" id="server_select_show_pass" style="width:auto;" />\r
+                                </td>\r
+                            </tr>\r
+\r
+                            <tr class="pass">\r
+                                <td><label for="server_select_password"><%= password %></label></td>\r
+                                <td><input type="password" class="password" id="server_select_password"></td>\r
+                            </tr>\r
+\r
+                            <tr class="channel">\r
+                                <td><label for="server_select_channel"><%= channel %></label></td>\r
+                                <td>\r
+                                    <div style="position:relative;">\r
+                                        <input type="text" class="channel" id="server_select_channel">\r
+                                        <i class="fa fa-key" title="<%= channel_key %>"></i>\r
+                                    </div>\r
+                                </td>\r
+                            </tr>\r
+\r
+                            <tr class="have_key">\r
+                                <td colspan="2">\r
+                                    <label for="server_select_show_channel_key"><%= require_key %></label> <input type="checkbox" id="server_select_show_channel_key" style="width:auto;" />\r
+                                </td>\r
+                            </tr>\r
+\r
+                            <tr class="key">\r
+                                <td><label for="server_select_channel_key"><%= key %></label></td>\r
+                                <td><input type="password" class="channel_key" id="server_select_channel_key"></td>\r
+                            </tr>\r
+\r
+                            <tr class="start">\r
+                                <td></td>\r
+                                <td><button type="submit"><%= start %></button></td>\r
+                            </tr>\r
+                        </table>\r
+\r
+                        <a href="" onclick="return false;" class="show_more"><%= server_network %> <i class="fa fa-caret-down"></i></a>\r
+                    </div>\r
+\r
+\r
+                    <div class="more">\r
+                        <table>\r
+                            <tr class="server">\r
+                                <td><label for="server_select_server"><%= server %></label></td>\r
+                                <td><input type="text" class="server" id="server_select_server"></td>\r
+                            <tr>\r
+\r
+                            <tr class="port">\r
+                                <td><label for="server_select_port"><%= port %></label></td>\r
+                                <td><input type="text" class="port" id="server_select_port"></td>\r
+                            </tr>\r
+\r
+                            <tr class="ssl">\r
+                                <td><label for="server_select_ssl">SSL</label></td>\r
+                                <td><input type="checkbox" class="ssl" id="server_select_ssl"></td>\r
+                            </tr>\r
+                        </table>\r
+                    </div>\r
+                </form>\r
+\r
+                <a class="kiwi_logo" href="https://kiwiirc.com/" target="_blank">\r
+                    <h1><span><%= powered_by %></span> <img src="/kiwi/assets/img/ico.png" alt="KiwiIRC Logo" title="Kiwi IRC" /></h1>\r
+                </a>\r
+            </div>\r
+        </div>\r
+    </script>\r
+\r
+    <script type="text/html" id="tmpl_theme_thumbnail">\r
+        <a class="thumbnail" data-setting="theme" data-value="<%= name.toLowerCase() %>" href="#">\r
+            <div class="thumbnail_wrapper"><div class="theme_color" style="background-color: <%= thumbnail_colour %>;"></div></div>\r
+            <div class="caption"><u><%= name %></u></div>\r
+        </a>\r
+    </script>\r
+\r
+    <script type="text/html" id="tmpl_applet_settings">\r
+        <div class="settings_container">\r
+            <form>\r
+                <section>\r
+                    <h6>Theme</h6>\r
+                    <div class="control-group">\r
+                        <div class="thumbnails">\r
+                            <% _.forEach(theme_thumbnails, function(thumbnail) { %>\r
+                                <%= thumbnail %>\r
+                            <% }); %>\r
+                        </div>\r
+                    </div>\r
+                </section>\r
+\r
+                <section>\r
+                    <h6>Channels</h6>\r
+                    <div class="control-group">\r
+                        <div class="radio">\r
+                            <label>\r
+                                <input type="radio" name="channel_list_style" data-setting="channel_list_style" value="tabs">\r
+                                <%= tabs %>\r
+                            </label>\r
+                        </div>\r
+                        <div class="radio">\r
+                            <label>\r
+                                <input type="radio" name="channel_list_style" data-setting="channel_list_style" value="list">\r
+                                <%= list %><small class="text-muted">(<%= large_amounts_of_chans %>)</small>\r
+                            </label>\r
+                        </div>\r
+                    </div>\r
+                </section>\r
+\r
+                <section>\r
+                    <h6>Chat window</h6>\r
+                    <div class="control-group">\r
+                        <div class="checkbox">\r
+                            <label>\r
+                                <input data-setting="show_joins_parts" type="checkbox">\r
+                                <%= join_part %>\r
+                            </label>\r
+                        </div>\r
+                        <div class="checkbox">\r
+                            <label>\r
+                                <input data-setting="count_all_activity" type="checkbox">\r
+                                <%= count_all_activity %>\r
+                            </label>\r
+                        </div>\r
+                        <div class="checkbox">\r
+                            <label>\r
+                                <input data-setting="show_timestamps" type="checkbox">\r
+                                <%= timestamps %>\r
+                            </label>\r
+                        </div>\r
+                        <div class="checkbox">\r
+                            <label>\r
+                                <input data-setting="use_24_hour_timestamps" type="checkbox">\r
+                                <%= timestamp_24 %>\r
+                            </label>\r
+                        </div>\r
+                        <div class="checkbox">\r
+                            <label>\r
+                                <input data-setting="mute_sounds" type="checkbox">\r
+                                <%= mute %>\r
+                            </label>\r
+                        </div>\r
+                        <div class="checkbox">\r
+                            <label>\r
+                                <input data-setting="show_emoticons" type="checkbox">\r
+                                <%= emoticons %>\r
+                            </label>\r
+                        </div>\r
+                        <label>\r
+                            <input data-setting="scrollback" class="input-small" type="text" size="4" pattern="\d*">\r
+                            <span><%= scroll_history %></span>\r
+                        </label>\r
+                    </div>\r
+                </section>\r
+\r
+                <section class="language">\r
+                    <h6>Language</h6>\r
+                    <div class="control-group">\r
+                        <select data-setting="locale">\r
+                            <option value=""></li>\r
+                            <% _.forEach(languages, function(lang) { %>\r
+                                <option value="<%= lang.tag %>"><%= lang.language %></li>\r
+                            <% }); %>\r
+                        </select>\r
+                        <br>\r
+                        <small><%= locale_restart_needed %></small>\r
+                    </div>\r
+                </section>\r
+\r
+                <section class="protocol_handler">\r
+                    <h6><%= default_client %></h6>\r
+                    <div class="control-group">\r
+                        <button class="register_protocol"><%= make_default %></button>\r
+                        <br>\r
+                        <small><%= default_note %></small>\r
+                    </div>\r
+                </section>\r
+\r
+                <section class="notification_enabler">\r
+                    <h6><%= html5_notifications %></h6>\r
+                    <div class="control-group">\r
+                        <button class="enable_notifications"><%= enable_notifications %></button>\r
+                    </div>\r
+                </section>\r
+            </form>\r
+        </div>\r
+    </script>\r
+\r
+\r
+    <script type="text/html" id="tmpl_channel_list">\r
+        <div class="applet_chanlist">\r
+            <table>\r
+                <thead style="font-weight: bold;">\r
+                    <tr>\r
+                        <td><a class="channel_name_title"><%= channel_name %></a></td>\r
+                        <td><a class="users_title"><%= users %></a></td>\r
+                        <td style="padding-left: 2em;"><%= topic %></td>\r
+                    </tr>\r
+                </thead>\r
+                <tbody style="vertical-align: top;">\r
+                </tbody>\r
+            </table>\r
+        </div>\r
+    </script>\r
+\r
+\r
+    <script type="text/html" id="tmpl_script_editor">\r
+        <div style="height:100%;">\r
+            <style>\r
+                #kiwi .script_editor .se_toolbar { padding: 10px 20px; }\r
+                #kiwi .script_editor .se_toolbar span.status { margin-left:2em; font-style:italic; }\r
+                #kiwi .script_editor .se_toolbar button { height:30px; padding:0 1em; }\r
+                #kiwi .script_editor .se_toolbar button i { font-size:1.2em; margin-left:3px; }\r
+            </style>\r
+            <div class="script_editor" style="height:100%; position:relative;">\r
+                <div class="se_toolbar"><button class="btn_save"><%= save %><i class="fa fa-floppy-o"></i></button><span class="status"></span></div>\r
+                <div class="editor" style="position:absolute;top:50px;bottom:0px;left:0px;right:0px;"></div>\r
+            </div>\r
+        </div>\r
+    </script>\r
+\r
+\r
+    <script type="text/html" id="tmpl_notifications">\r
+        <div class="notification-inner">\r
+            <i class="fa fa-times close"></i>\r
+            <h6></h6>\r
+            <div class="content"></div>\r
+        </div>\r
+    </script>\r
+\r
+\r
+<script src="/kiwi/assets/libs/jquery-1.11.1.min.js"></script>\r
+<script>\r
+/*\r
+@licstart  The following is the entire license notice for the\r
+JavaScript code in this page.\r
+\r
+Copyright (C) 2014  Loic J. Duros\r
+\r
+The JavaScript code in this page is free software: you can\r
+redistribute it and/or modify it under the terms of the GNU\r
+General Public License (GNU GPL) as published by the Free Software\r
+Foundation, either version 3 of the License, or (at your option)\r
+any later version.  The code is distributed WITHOUT ANY WARRANTY;\r
+without even the implied warranty of MERCHANTABILITY or FITNESS\r
+FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.\r
+\r
+As additional permission under GNU GPL version 3 section 7, you\r
+may distribute non-source (e.g., minimized or compacted) forms of\r
+that code without the copy of the GNU GPL normally required by\r
+section 4, provided you include this license notice and a URL\r
+through which recipients can access the Corresponding Source.\r
+\r
+\r
+@licend  The above is the entire license notice\r
+for the JavaScript code in this page.\r
+*/\r
+/* Script loader (https://github.com/ded/script.js) */\r
+(function(a,b,c){typeof c["module"]!="undefined"&&c.module.exports?c.module.exports=b():typeof c["define"]!="undefined"&&c["define"]=="function"&&c.define.amd?define(a,b):c[a]=b()})("$script",function(){function p(a,b){for(var c=0,d=a.length;c<d;++c)if(!b(a[c]))return j;return 1}function q(a,b){p(a,function(a){return!b(a)})}function r(a,b,i){function o(a){return a.call?a():d[a]}function t(){if(!--n){d[m]=1,l&&l();for(var a in f)p(a.split("|"),o)&&!q(f[a],o)&&(f[a]=[])}}a=a[k]?a:[a];var j=b&&b.call,l=j?b:i,m=j?a.join(""):b,n=a.length;return setTimeout(function(){q(a,function(a){if(h[a])return m&&(e[m]=1),h[a]==2&&t();h[a]=1,m&&(e[m]=1),s(!c.test(a)&&g?g+a+".js":a,t)})},0),r}function s(c,d){var e=a.createElement("script"),f=j;e.onload=e.onerror=e[o]=function(){if(e[m]&&!/^c|loade/.test(e[m])||f)return;e.onload=e[o]=null,f=1,h[c]=2,d()},e.async=1,e.src=c,b.insertBefore(e,b.firstChild)}var a=document,b=a.getElementsByTagName("head")[0],c=/^https?:\/\//,d={},e={},f={},g,h={},i="string",j=!1,k="push",l="DOMContentLoaded",m="readyState",n="addEventListener",o="onreadystatechange";return!a[m]&&a[n]&&(a[n](l,function t(){a.removeEventListener(l,t,j),a[m]="complete"},j),a[m]="loading"),r.get=s,r.order=function(a,b,c){(function d(e){e=a.shift(),a.length?r(e,d):r(e,b,c)})()},r.path=function(a){g=a},r.ready=function(a,b,c){a=a[k]?a:[a];var e=[];return!q(a,function(a){d[a]||e[k](a)})&&p(a,function(a){return d[a]})?b():!function(a){f[a]=f[a]||[],f[a][k](b),c&&c(e)}(a.join("|")),r},r},this)\r
+\r
+\r
+    // Avoid `console` errors in browsers that lack a console. (https://github.com/h5bp/html5-boilerplate)\r
+    function normalizeConsole() {\r
+        var method;\r
+        var noop = function () {};\r
+        var methods = [\r
+            'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error',\r
+            'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log',\r
+            'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd',\r
+            'timeStamp', 'trace', 'warn'\r
+        ];\r
+        var length = methods.length;\r
+        var console = (window.console = window.console || {});\r
+\r
+        while (length--) {\r
+            method = methods[length];\r
+\r
+            // Only stub undefined methods.\r
+            if (!console[method]) {\r
+                console[method] = noop;\r
+            }\r
+        }\r
+    }\r
+\r
+    normalizeConsole();\r
+\r
+    function getQueryVariable(variable) {\r
+        var query = window.location.search.substring(1);\r
+        var vars = query.split('&');\r
+        for (var i = 0; i < vars.length; i++) {\r
+            var pair = vars[i].replace(/\+/g, '%20').split('=');\r
+            if (decodeURIComponent(pair[0]) == variable) {\r
+                return decodeURIComponent(pair[1]);\r
+            }\r
+        }\r
+    }\r
+\r
+    (function afterPromiseAvailable() {\r
+        var base_path = '/kiwi', // Entry path for the kiwi application\r
+            scripts = [],\r
+            opts = {\r
+                container: $('body'),\r
+                base_path: base_path,\r
+                settings_path: base_path + '/assets/settings.json'\r
+            },\r
+            script_promise, script_promise_resolve,\r
+            onload_promise,\r
+            settings_promise;\r
+\r
+        // If the browser doesn't natively support promises load up the polyfill and try again.\r
+        if (!window.Promise) {\r
+            $script(base_path + "/assets/libs/promise.min.js", afterPromiseAvailable);\r
+            return;\r
+        }\r
+\r
+        function loadScripts(scripts) {\r
+            return new Promise(function (resolve, reject) {\r
+                var to_load, idx,\r
+                    base = base_path + '/';\r
+                if (typeof scripts === 'string') {\r
+                    to_load = base + scripts;\r
+                } else {\r
+                    to_load = [];\r
+                    for (idx in scripts) {\r
+                        to_load.push(base + scripts[idx]);\r
+                    }\r
+                }\r
+                $script(to_load, resolve, reject);\r
+            });\r
+        }\r
+\r
+        onload_promise = new Promise(function (resolve) {\r
+            // Document may already be loaded if we had to load the Promise shim seperately\r
+            if (document.readyState === 'complete') {\r
+                resolve();\r
+                return;\r
+            }\r
+\r
+            window.onload = resolve;\r
+        });\r
+\r
+        // Get a resolve function for the script loading promises\r
+        script_promise = new Promise(function (resolve) {\r
+            script_promise_resolve = resolve;\r
+        });\r
+\r
+        // Chain each script loading promise\r
+        script_promise = script_promise.then(function () {\r
+            var idx;\r
+            for (idx = 0; idx < scripts.length; idx++) {\r
+                (function (idx) {\r
+                    script_promise = script_promise.then(function () {\r
+                        return loadScripts(scripts[idx]);\r
+                    });\r
+                })(idx);\r
+            }\r
+        });\r
+\r
+        // Debugging will get a list of debugging scripts from settings.json (below)\r
+        if (!getQueryVariable('debug')) {\r
+            scripts.push(['assets/libs/lodash.min.js?t=1458242884965']);\r
+            scripts.push([\r
+                'assets/libs/backbone.min.js?t=1458242884965',\r
+                'assets/libs/jed.js?t=1458242884965'\r
+            ]);\r
+            scripts.push([\r
+                'assets/kiwi.min.js?t=1458242884965',\r
+                'assets/libs/engine.io.bundle.min.js?t=1458242884965'\r
+            ]);\r
+\r
+            script_promise_resolve();\r
+        }\r
+\r
+        settings_promise = new Promise(function (resolve) {\r
+            $.getJSON(opts.settings_path, function (data) {\r
+                opts.server_settings = data.server_settings;\r
+                opts.client_plugins = data.client_plugins;\r
+                opts.translations = data.translations;\r
+                opts.locale = data.locale;\r
+                opts.themes = data.themes;\r
+\r
+                if (typeof data.kiwi_server !== 'undefined') {\r
+                    opts.kiwi_server = data.kiwi_server;\r
+                }\r
+\r
+                resolve();\r
+\r
+                // If debugging, grab the debug scripts and load them\r
+                if (getQueryVariable('debug')) {\r
+                    scripts = scripts.concat(data.scripts);\r
+                    script_promise_resolve();\r
+                }\r
+\r
+                // Load themes\r
+                if (opts.themes) {\r
+                    $.each(opts.themes, function (theme_idx, theme) {\r
+                        var disabled = (opts.server_settings.client.settings.theme.toLowerCase() !== theme.name.toLowerCase()),\r
+                            rel = (disabled?'alternate ':'') + 'stylesheet';\r
+\r
+                        var link = $.parseHTML('<link rel="' + rel + '" type="text/css" data-theme href="'+ opts.base_path + '/assets/themes/' + theme.name.toLowerCase() + '/style.css" title="' + theme.name.toLowerCase() + '" ' + (disabled?'disabled':'') + '/>');\r
+                        link.disabled = disabled;\r
+\r
+                        $(link).appendTo($('head'));\r
+                    });\r
+                }\r
+            });\r
+        });\r
+\r
+        // prawnsalad: Why is the below script_promise in its own .then()?\r
+        // M2Ys4U: prawnsalad: either of the first two promises can resolve the\r
+        // third one, but the third one has a then() on it already, so the\r
+        // then() on the all() makes it wait for the then() on the script promise\r
+        // to resolve.\r
+        //\r
+        // Promises - simple.\r
+        Promise.all([onload_promise, settings_promise])\r
+        .then(function(){return script_promise})\r
+        .then(function startApp() {\r
+            // Kiwi IRC version this is built from\r
+            kiwi.build_version = '0.9.0';\r
+\r
+            // Start the app after loading plugins\r
+            kiwi.init(opts, function() {\r
+                if (opts.client_plugins && opts.client_plugins.length > 0) {\r
+\r
+                    // Wait until all plugins are loaded before starting the app\r
+                    kiwi.plugins.once('loaded', function() {\r
+                        kiwi.start();\r
+                    });\r
+\r
+                    _.each(opts.client_plugins, function (plugin_url) {\r
+                        kiwi.plugins.load(plugin_url);\r
+                    });\r
+\r
+                } else {\r
+\r
+                    // No plugins were needed so start the app\r
+                    kiwi.start();\r
+                }\r
+            });\r
+        });\r
+    })();\r
+</script>\r
+</body>\r
+</html>\r
diff --git a/client/weblabels.html b/client/weblabels.html
new file mode 100644 (file)
index 0000000..65c4d59
--- /dev/null
@@ -0,0 +1,189 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+                 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+  <head>
+       <title>libreplanet.org - JavaScript License Information</title>
+  </head>
+
+  <body>
+
+    <table id="jslicense-labels1">
+
+      <tr>
+       <td><a href="/2016/assets/js/lodash.min.js">lodash.min.js</a></td>
+       <td><a href="https://lodash.com/license">Expat</a></td>
+       <td><a href="/2016/assets/js/lodash.js">lodash.js</a></td>
+      </tr>
+
+      <tr>
+       <td><a href="/2016/assets/js/kiwi.min.js">kiwi.min.js</a></td>
+        <td><a href="http://www.gnu.org/licenses/agpl-3.0.html">GNU Affero General Public License version 3</a></td>
+       <td><a href="/2016/assets/js/kiwi.js">kiwi.js</a></td>
+      </tr>
+
+
+      <tr>
+       <td><a href="/assets/libs/engine.io.bundle.min.js">engine.io.bundle.min.js</a></td>
+       <td><a href="http://www.gnu.org/licenses/agpl-3.0.html">GNU Affero General Public License version 3</a></td>
+       <td><a href="/assets/libs/engine.io.bundle.js">engine.io.bundle.js</a></td>
+      </tr>
+
+
+      
+      
+      
+      
+
+         <tr>
+               <td><a href="/2015/assets/js/jquery-1.8.3.min.js">jquery-1.8.3.min.js</a></td>
+               <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+               <td><a href="/2015/assets/js/jquery-1.8.3.js">jquery-1.8.3.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/jquery-1.11.1.min.js">jquery-1.11.1.min.js</a></td>
+               <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+               <td><a href="/2015/assets/js/jquery-1.11.1.js">jquery-1.11.1.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/bootstrap.min.js">bootstrap.min.js</a></td>
+               <td><a href="http://www.apache.org/licenses/LICENSE-2.0">Apache-2.0-only</a></td>
+                 <td><a href="/2015/assets/js/bootstrap.js">bootstrap.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/bootstrap-3.2.0.min.js">bootstrap-3.2.0.min.js</a></td>
+               <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+               <td><a href="/2015/assets/js/bootstrap-3.2.0.js">bootstrap-3.2.0.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="https://libreplanet.org/w/skins/common/wikibits.js">wikibits.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="https://libreplanet.org/w/skins/common/wikibits.js">wikibits.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="https://libreplanet.org/w/skins/common/ajax.js">ajax.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="https://libreplanet.org/w/skins/common/ajax.js">ajax.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="https://libreplanet.org/w/skins/common/ajaxwatch.js">ajaxwatch.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="https://libreplanet.org/w/skins/common/ajaxwatch.js">ajaxwatch.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="https://libreplanet.org/w/skins/common/mwsuggest.js">mwsuggest.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="https://libreplanet.org/w/skins/common/mwsuggest.js">mwsuggest.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="https://libreplanet.org/w/skins/common/edit.js">edit.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="https://libreplanet.org/w/skins/common/edit.js">edit.js</a></td>
+         </tr>
+
+
+         <tr>
+               <td><a href="http://libreplanet.org/wiki?title=-&amp;action=raw&amp;smaxage=0&amp;gen=js&amp;useskin=vector&amp;270">wiki</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="http://weblabels.fsf.org/libreplanet.org/CURRENT/files/w/wiki">wiki</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="http://libreplanet.org/2014/assets/js/jquery.chainedSelects.js">jquery.chainedSelects.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-3.0.html">GNU General
+                       Public License version 3 or later</a></td>
+               <td><a href="http://libreplanet.org/2014/assets/js/jquery.chainedSelects.js">jquery.chainedSelects.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/civicrm-4.4.jquery.chainedSelects.js">civicrm-4.4.jquery.chainedSelects.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-3.0.html">GNU General
+                       Public License version 3 or later</a></td>
+               <td><a href="/2015/assets/js/civicrm-4.4.jquery.chainedSelects.js">civicrm-4.4.jquery.chainedSelects.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="http://libreplanet.org/2014/assets/js/jquery.form.js">jquery.form.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="http://libreplanet.org/2014/assets/js/jquery.form.js">jquery.form.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/jquery.form-3.25.0.js">jquery.form-3.25.0.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="/2015/assets/js/jquery.form-3.25.0.js">jquery.form-3.25.0.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/jquery.timeentry.pack.js">jquery.timeentry.pack.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="/2015/assets/js/jquery.timeentry.pack.js">jquery.timeentry.pack.js</a></td>
+         </tr>
+
+         <tr>
+                 <td><a href="http://libreplanet.org/2014/assets/js/Common.js">Common.js</a></td>
+                 <td><a href="http://www.gnu.org/licenses/agpl-3.0.html">GNU Affero
+                         General Public License version 3</a></td>
+                 <td><a href="http://libreplanet.org/2014/assets/js/Common.js">Common.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/civicrm-4.4.Common.js">civicrm-4.4.Common.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/agpl-3.0.html">GNU Affero
+                       General Public License version 3</a></td>
+               <td><a href="/2015/assets/js/civicrm-4.4.Common.js">civicrm-4.4.Common.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/jquery-ui-1.9.0.custom.min.js">jquery-ui-1.9.0.custom.min.js</a></td>
+               <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+               <td><a href="/2015/assets/js/jquery-ui-1.9.0.custom.js">jquery-ui-1.9.0.custom.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/moment.js">moment.js</a></td>
+               <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+               <td><a href="/2015/assets/js/moment.js">moment.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/jquery.feeds.min.js">jquery.feeds.min.js</a></td>
+               <td>
+                 <a href="http://www.jclark.com/xml/copying.txt">Expat</a> <br/>
+                 <a href="http://www.gnu.org/licenses/gpl.html">GNU General
+                       Public License version 3 or later</a>
+               </td>
+               <td><a href="/2015/assets/js/jquery.feeds.js">jquery.feeds.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/tweetie/tweetie.js">tweetie.js</a></td>
+               <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+               <td><a href="/2015/assets/js/tweetie/tweetie.js">tweetie.js</a></td>
+         </tr>
+
+          <tr>
+           <td><a href="/2015/assets/js/mithril.min.js">mithril.min.js</a></td>
+           <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+           <td><a href="/2015/assets/js/mithril.js">mithril.js</a></td>
+         </tr>
+
+       </table>
+  </body>
+</html>
diff --git a/client/weblabels.html~ b/client/weblabels.html~
new file mode 100644 (file)
index 0000000..b0c3455
--- /dev/null
@@ -0,0 +1,189 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+                 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+  <head>
+       <title>libreplanet.org - JavaScript License Information</title>
+  </head>
+
+  <body>
+
+    <table id="jslicense-labels1">
+
+      <tr>
+       <td><a href="/2016/assets/js/lodash.min.js">lodash.min.js</a></td>
+       <td><a href="https://lodash.com/license">Expat</a></td>
+       <td><a href="/2016/assets/js/lodash.js">lodash.js</a></td>
+      </tr>
+
+      <tr>
+       <td><a href="/2016/assets/js/kiwi.min.js">kiwi.min.js</a></td>
+        <td><a href="http://www.gnu.org/licenses/agpl-3.0.html">GNU Affero General Public License version 3</a></td>
+       <td><a href="/2016/assets/js/kiwi.js">kiwi.js</a></td>
+      </tr>
+
+
+      <tr>
+       <td><a href="/2016/assets/js/engine.io.bundle.min.js">engine.io.bundle.min.js</a></td>
+       <td><a href="http://www.gnu.org/licenses/agpl-3.0.html">GNU Affero General Public License version 3</a></td>
+       <td><a href="/2016/assets/js/engine.io.bundle.js">engine.io.bundle.js</a></td>
+      </tr>
+
+
+      
+      
+      
+      
+
+         <tr>
+               <td><a href="/2015/assets/js/jquery-1.8.3.min.js">jquery-1.8.3.min.js</a></td>
+               <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+               <td><a href="/2015/assets/js/jquery-1.8.3.js">jquery-1.8.3.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/jquery-1.11.1.min.js">jquery-1.11.1.min.js</a></td>
+               <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+               <td><a href="/2015/assets/js/jquery-1.11.1.js">jquery-1.11.1.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/bootstrap.min.js">bootstrap.min.js</a></td>
+               <td><a href="http://www.apache.org/licenses/LICENSE-2.0">Apache-2.0-only</a></td>
+                 <td><a href="/2015/assets/js/bootstrap.js">bootstrap.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/bootstrap-3.2.0.min.js">bootstrap-3.2.0.min.js</a></td>
+               <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+               <td><a href="/2015/assets/js/bootstrap-3.2.0.js">bootstrap-3.2.0.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="https://libreplanet.org/w/skins/common/wikibits.js">wikibits.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="https://libreplanet.org/w/skins/common/wikibits.js">wikibits.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="https://libreplanet.org/w/skins/common/ajax.js">ajax.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="https://libreplanet.org/w/skins/common/ajax.js">ajax.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="https://libreplanet.org/w/skins/common/ajaxwatch.js">ajaxwatch.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="https://libreplanet.org/w/skins/common/ajaxwatch.js">ajaxwatch.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="https://libreplanet.org/w/skins/common/mwsuggest.js">mwsuggest.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="https://libreplanet.org/w/skins/common/mwsuggest.js">mwsuggest.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="https://libreplanet.org/w/skins/common/edit.js">edit.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="https://libreplanet.org/w/skins/common/edit.js">edit.js</a></td>
+         </tr>
+
+
+         <tr>
+               <td><a href="http://libreplanet.org/wiki?title=-&amp;action=raw&amp;smaxage=0&amp;gen=js&amp;useskin=vector&amp;270">wiki</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="http://weblabels.fsf.org/libreplanet.org/CURRENT/files/w/wiki">wiki</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="http://libreplanet.org/2014/assets/js/jquery.chainedSelects.js">jquery.chainedSelects.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-3.0.html">GNU General
+                       Public License version 3 or later</a></td>
+               <td><a href="http://libreplanet.org/2014/assets/js/jquery.chainedSelects.js">jquery.chainedSelects.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/civicrm-4.4.jquery.chainedSelects.js">civicrm-4.4.jquery.chainedSelects.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-3.0.html">GNU General
+                       Public License version 3 or later</a></td>
+               <td><a href="/2015/assets/js/civicrm-4.4.jquery.chainedSelects.js">civicrm-4.4.jquery.chainedSelects.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="http://libreplanet.org/2014/assets/js/jquery.form.js">jquery.form.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="http://libreplanet.org/2014/assets/js/jquery.form.js">jquery.form.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/jquery.form-3.25.0.js">jquery.form-3.25.0.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="/2015/assets/js/jquery.form-3.25.0.js">jquery.form-3.25.0.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/jquery.timeentry.pack.js">jquery.timeentry.pack.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="/2015/assets/js/jquery.timeentry.pack.js">jquery.timeentry.pack.js</a></td>
+         </tr>
+
+         <tr>
+                 <td><a href="http://libreplanet.org/2014/assets/js/Common.js">Common.js</a></td>
+                 <td><a href="http://www.gnu.org/licenses/agpl-3.0.html">GNU Affero
+                         General Public License version 3</a></td>
+                 <td><a href="http://libreplanet.org/2014/assets/js/Common.js">Common.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/civicrm-4.4.Common.js">civicrm-4.4.Common.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/agpl-3.0.html">GNU Affero
+                       General Public License version 3</a></td>
+               <td><a href="/2015/assets/js/civicrm-4.4.Common.js">civicrm-4.4.Common.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/jquery-ui-1.9.0.custom.min.js">jquery-ui-1.9.0.custom.min.js</a></td>
+               <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+               <td><a href="/2015/assets/js/jquery-ui-1.9.0.custom.js">jquery-ui-1.9.0.custom.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/moment.js">moment.js</a></td>
+               <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+               <td><a href="/2015/assets/js/moment.js">moment.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/jquery.feeds.min.js">jquery.feeds.min.js</a></td>
+               <td>
+                 <a href="http://www.jclark.com/xml/copying.txt">Expat</a> <br/>
+                 <a href="http://www.gnu.org/licenses/gpl.html">GNU General
+                       Public License version 3 or later</a>
+               </td>
+               <td><a href="/2015/assets/js/jquery.feeds.js">jquery.feeds.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/tweetie/tweetie.js">tweetie.js</a></td>
+               <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+               <td><a href="/2015/assets/js/tweetie/tweetie.js">tweetie.js</a></td>
+         </tr>
+
+          <tr>
+           <td><a href="/2015/assets/js/mithril.min.js">mithril.min.js</a></td>
+           <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+           <td><a href="/2015/assets/js/mithril.js">mithril.js</a></td>
+         </tr>
+
+       </table>
+  </body>
+</html>
diff --git a/weblabels.html b/weblabels.html
new file mode 100644 (file)
index 0000000..65c4d59
--- /dev/null
@@ -0,0 +1,189 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+                 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+  <head>
+       <title>libreplanet.org - JavaScript License Information</title>
+  </head>
+
+  <body>
+
+    <table id="jslicense-labels1">
+
+      <tr>
+       <td><a href="/2016/assets/js/lodash.min.js">lodash.min.js</a></td>
+       <td><a href="https://lodash.com/license">Expat</a></td>
+       <td><a href="/2016/assets/js/lodash.js">lodash.js</a></td>
+      </tr>
+
+      <tr>
+       <td><a href="/2016/assets/js/kiwi.min.js">kiwi.min.js</a></td>
+        <td><a href="http://www.gnu.org/licenses/agpl-3.0.html">GNU Affero General Public License version 3</a></td>
+       <td><a href="/2016/assets/js/kiwi.js">kiwi.js</a></td>
+      </tr>
+
+
+      <tr>
+       <td><a href="/assets/libs/engine.io.bundle.min.js">engine.io.bundle.min.js</a></td>
+       <td><a href="http://www.gnu.org/licenses/agpl-3.0.html">GNU Affero General Public License version 3</a></td>
+       <td><a href="/assets/libs/engine.io.bundle.js">engine.io.bundle.js</a></td>
+      </tr>
+
+
+      
+      
+      
+      
+
+         <tr>
+               <td><a href="/2015/assets/js/jquery-1.8.3.min.js">jquery-1.8.3.min.js</a></td>
+               <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+               <td><a href="/2015/assets/js/jquery-1.8.3.js">jquery-1.8.3.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/jquery-1.11.1.min.js">jquery-1.11.1.min.js</a></td>
+               <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+               <td><a href="/2015/assets/js/jquery-1.11.1.js">jquery-1.11.1.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/bootstrap.min.js">bootstrap.min.js</a></td>
+               <td><a href="http://www.apache.org/licenses/LICENSE-2.0">Apache-2.0-only</a></td>
+                 <td><a href="/2015/assets/js/bootstrap.js">bootstrap.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/bootstrap-3.2.0.min.js">bootstrap-3.2.0.min.js</a></td>
+               <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+               <td><a href="/2015/assets/js/bootstrap-3.2.0.js">bootstrap-3.2.0.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="https://libreplanet.org/w/skins/common/wikibits.js">wikibits.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="https://libreplanet.org/w/skins/common/wikibits.js">wikibits.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="https://libreplanet.org/w/skins/common/ajax.js">ajax.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="https://libreplanet.org/w/skins/common/ajax.js">ajax.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="https://libreplanet.org/w/skins/common/ajaxwatch.js">ajaxwatch.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="https://libreplanet.org/w/skins/common/ajaxwatch.js">ajaxwatch.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="https://libreplanet.org/w/skins/common/mwsuggest.js">mwsuggest.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="https://libreplanet.org/w/skins/common/mwsuggest.js">mwsuggest.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="https://libreplanet.org/w/skins/common/edit.js">edit.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="https://libreplanet.org/w/skins/common/edit.js">edit.js</a></td>
+         </tr>
+
+
+         <tr>
+               <td><a href="http://libreplanet.org/wiki?title=-&amp;action=raw&amp;smaxage=0&amp;gen=js&amp;useskin=vector&amp;270">wiki</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="http://weblabels.fsf.org/libreplanet.org/CURRENT/files/w/wiki">wiki</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="http://libreplanet.org/2014/assets/js/jquery.chainedSelects.js">jquery.chainedSelects.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-3.0.html">GNU General
+                       Public License version 3 or later</a></td>
+               <td><a href="http://libreplanet.org/2014/assets/js/jquery.chainedSelects.js">jquery.chainedSelects.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/civicrm-4.4.jquery.chainedSelects.js">civicrm-4.4.jquery.chainedSelects.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-3.0.html">GNU General
+                       Public License version 3 or later</a></td>
+               <td><a href="/2015/assets/js/civicrm-4.4.jquery.chainedSelects.js">civicrm-4.4.jquery.chainedSelects.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="http://libreplanet.org/2014/assets/js/jquery.form.js">jquery.form.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="http://libreplanet.org/2014/assets/js/jquery.form.js">jquery.form.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/jquery.form-3.25.0.js">jquery.form-3.25.0.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="/2015/assets/js/jquery.form-3.25.0.js">jquery.form-3.25.0.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/jquery.timeentry.pack.js">jquery.timeentry.pack.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General
+                       Public License version 2 or later</a></td>
+               <td><a href="/2015/assets/js/jquery.timeentry.pack.js">jquery.timeentry.pack.js</a></td>
+         </tr>
+
+         <tr>
+                 <td><a href="http://libreplanet.org/2014/assets/js/Common.js">Common.js</a></td>
+                 <td><a href="http://www.gnu.org/licenses/agpl-3.0.html">GNU Affero
+                         General Public License version 3</a></td>
+                 <td><a href="http://libreplanet.org/2014/assets/js/Common.js">Common.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/civicrm-4.4.Common.js">civicrm-4.4.Common.js</a></td>
+               <td><a href="http://www.gnu.org/licenses/agpl-3.0.html">GNU Affero
+                       General Public License version 3</a></td>
+               <td><a href="/2015/assets/js/civicrm-4.4.Common.js">civicrm-4.4.Common.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/jquery-ui-1.9.0.custom.min.js">jquery-ui-1.9.0.custom.min.js</a></td>
+               <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+               <td><a href="/2015/assets/js/jquery-ui-1.9.0.custom.js">jquery-ui-1.9.0.custom.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/moment.js">moment.js</a></td>
+               <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+               <td><a href="/2015/assets/js/moment.js">moment.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/jquery.feeds.min.js">jquery.feeds.min.js</a></td>
+               <td>
+                 <a href="http://www.jclark.com/xml/copying.txt">Expat</a> <br/>
+                 <a href="http://www.gnu.org/licenses/gpl.html">GNU General
+                       Public License version 3 or later</a>
+               </td>
+               <td><a href="/2015/assets/js/jquery.feeds.js">jquery.feeds.js</a></td>
+         </tr>
+
+         <tr>
+               <td><a href="/2015/assets/js/tweetie/tweetie.js">tweetie.js</a></td>
+               <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+               <td><a href="/2015/assets/js/tweetie/tweetie.js">tweetie.js</a></td>
+         </tr>
+
+          <tr>
+           <td><a href="/2015/assets/js/mithril.min.js">mithril.min.js</a></td>
+           <td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
+           <td><a href="/2015/assets/js/mithril.js">mithril.js</a></td>
+         </tr>
+
+       </table>
+  </body>
+</html>