Client: view.js split up into multiple files
authorDarren <darren@Darrens-MacBook-Pro.local>
Wed, 22 May 2013 19:49:46 +0000 (20:49 +0100)
committerDarren <darren@Darrens-MacBook-Pro.local>
Wed, 22 May 2013 19:49:46 +0000 (20:49 +0100)
21 files changed:
client/assets/src/build.js
client/assets/src/index.html.tmpl
client/assets/src/views/applet.js [new file with mode: 0644]
client/assets/src/views/application.js [new file with mode: 0644]
client/assets/src/views/apptoolbar.js [new file with mode: 0644]
client/assets/src/views/channel.js [new file with mode: 0644]
client/assets/src/views/controlbox.js [new file with mode: 0644]
client/assets/src/views/mediamessage.js [new file with mode: 0644]
client/assets/src/views/member.js [new file with mode: 0644]
client/assets/src/views/memberlist.js [new file with mode: 0644]
client/assets/src/views/menubox.js [new file with mode: 0644]
client/assets/src/views/networktabs.js [new file with mode: 0644]
client/assets/src/views/nickchangebox.js [new file with mode: 0644]
client/assets/src/views/panel.js [new file with mode: 0644]
client/assets/src/views/resizehandler.js [new file with mode: 0644]
client/assets/src/views/serverselect.js [new file with mode: 0644]
client/assets/src/views/statusmessage.js [new file with mode: 0644]
client/assets/src/views/tabs.js [new file with mode: 0644]
client/assets/src/views/topicbar.js [new file with mode: 0644]
client/assets/src/views/userbox.js [new file with mode: 0644]
client/assets/src/views/view.js [deleted file]

index d8ceedf42537a1321f929fe912fd5f9ecb8cbca3..87d66072c8ba46ef53341c1b8b0a49e9f96a5c8d 100644 (file)
@@ -51,7 +51,25 @@ var src = concat([
     __dirname + '/applets/scripteditor.js',\r
 \r
     __dirname + '/utils.js',\r
-    __dirname + '/views/view.js'\r
+\r
+    __dirname + '/views/panel.js',\r
+    __dirname + '/views/channel.js',\r
+    __dirname + '/views/applet.js',\r
+    __dirname + '/views/application.js',\r
+    __dirname + '/views/apptoolbar.js',\r
+    __dirname + '/views/controlbox.js',\r
+    __dirname + '/views/mediamessage.js',\r
+    __dirname + '/views/member.js',\r
+    __dirname + '/views/memberlist.js',\r
+    __dirname + '/views/menubox.js',\r
+    __dirname + '/views/networktabs.js',\r
+    __dirname + '/views/nickchangebox.js',\r
+    __dirname + '/views/resizehandler.js',\r
+    __dirname + '/views/serverselect.js',\r
+    __dirname + '/views/statusmessage.js',\r
+    __dirname + '/views/tabs.js',\r
+    __dirname + '/views/topicbar.js',\r
+    __dirname + '/views/userbox.js'\r
 ]);\r
 \r
 \r
index 17f1bbaf2217b908a254a719221244abc1e15c85..d06e1000c9f790d953d52eacb8d93432822eaee0 100644 (file)
                     'src/models/datastore.js',\r
                     'src/utils.js',\r
                     'src/views/view.js'\r
+                ],\r
+\r
+                // Some views extend these, so make sure they're loaded beforehand\r
+                [\r
+                    'src/views/panel.js'\r
+                ],\r
+\r
+                [\r
+                    'src/views/channel.js',\r
+                    'src/views/applet.js',\r
+                    'src/views/application.js',\r
+                    'src/views/apptoolbar.js',\r
+                    'src/views/controlbox.js',\r
+                    'src/views/mediamessage.js',\r
+                    'src/views/member.js',\r
+                    'src/views/memberlist.js',\r
+                    'src/views/menubox.js',\r
+                    'src/views/networktabs.js',\r
+                    'src/views/nickchangebox.js',\r
+                    'src/views/resizehandler.js',\r
+                    'src/views/serverselect.js',\r
+                    'src/views/statusmessage.js',\r
+                    'src/views/tabs.js',\r
+                    'src/views/topicbar.js',\r
+                    'src/views/userbox.js'\r
                 ]\r
             ]);\r
         } else {\r
diff --git a/client/assets/src/views/applet.js b/client/assets/src/views/applet.js
new file mode 100644 (file)
index 0000000..383f5c8
--- /dev/null
@@ -0,0 +1,6 @@
+_kiwi.view.Applet = _kiwi.view.Panel.extend({
+    className: 'panel applet',
+    initialize: function (options) {
+        this.initializePanel(options);
+    }
+});
\ No newline at end of file
diff --git a/client/assets/src/views/application.js b/client/assets/src/views/application.js
new file mode 100644 (file)
index 0000000..bb72eb7
--- /dev/null
@@ -0,0 +1,288 @@
+_kiwi.view.Application = Backbone.View.extend({
+    initialize: function () {
+        var that = this;
+
+        $(window).resize(function() { that.doLayout.apply(that); });
+        this.$el.find('.toolbar').resize(function() { that.doLayout.apply(that); });
+        $('#kiwi .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.doLayout();
+
+        $(document).keydown(this.setKeyFocus);
+
+        // Confirmation require to leave the page
+        window.onbeforeunload = function () {
+            if (_kiwi.gateway.isConnected()) {
+                return 'This will close all KiwiIRC conversations. Are you sure you want to close this window?';
+            }
+        };
+
+        this.initSound();
+    },
+
+
+
+    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');
+
+        // Clear any current theme
+        this.$el.removeClass(function (i, css) {
+            return (css.match(/\btheme_\S+/g) || []).join(' ');
+        });
+
+        // Apply the new theme
+        this.$el.addClass('theme_' + (theme_name || 'relaxed'));
+    },
+
+
+    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 el_kiwi = this.$el;
+        var el_panels = $('#kiwi .panels');
+        var el_memberlists = $('#kiwi .memberlists');
+        var el_toolbar = this.$el.find('.toolbar');
+        var el_controlbox = $('#kiwi .controlbox');
+        var el_resize_handle = $('#kiwi .memberlists_resize_handle');
+
+        var css_heights = {
+            top: el_toolbar.outerHeight(true),
+            bottom: el_controlbox.outerHeight(true)
+        };
+
+
+        // If any elements are not visible, full size the panals instead
+        if (!el_toolbar.is(':visible')) {
+            css_heights.top = 0;
+        }
+
+        if (!el_controlbox.is(':visible')) {
+            css_heights.bottom = 0;
+        }
+
+        // Apply the CSS sizes
+        el_panels.css(css_heights);
+        el_memberlists.css(css_heights);
+        el_resize_handle.css(css_heights);
+
+        // If we have channel tabs on the side, adjust the height
+        if (el_kiwi.hasClass('chanlist_treeview')) {
+            this.$el.find('.tabs', el_kiwi).css(css_heights);
+        }
+
+        // Determine if we have a narrow window (mobile/tablet/or even small desktop window)
+        if (el_kiwi.outerWidth() < 400) {
+            el_kiwi.addClass('narrow');
+        } else {
+            el_kiwi.removeClass('narrow');
+        }
+
+        // Set the panels width depending on the memberlist visibility
+        if (el_memberlists.css('display') != 'none') {
+            // Panels to the side of the memberlist
+            el_panels.css('right', el_memberlists.outerWidth(true));
+            // The resize handle sits overlapping the panels and memberlist
+            el_resize_handle.css('left', el_memberlists.position().left - (el_resize_handle.outerWidth(true) / 2));
+        } else {
+            // Memberlist is hidden so panels to the right edge
+            el_panels.css('right', 0);
+            // And move the handle just out of sight to the right
+            el_resize_handle.css('left', el_panels.outerWidth(true));
+        }
+
+        var input_wrap_width = parseInt($('#kiwi .controlbox .input_tools').outerWidth());
+        el_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 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);
+    }
+});
\ No newline at end of file
diff --git a/client/assets/src/views/apptoolbar.js b/client/assets/src/views/apptoolbar.js
new file mode 100644 (file)
index 0000000..1fc5148
--- /dev/null
@@ -0,0 +1,12 @@
+_kiwi.view.AppToolbar = Backbone.View.extend({
+    events: {
+        'click .settings': 'clickSettings'
+    },
+
+    initialize: function () {
+    },
+
+    clickSettings: function (event) {
+        _kiwi.app.controlbox.processInput('/settings');
+    }
+});
\ No newline at end of file
diff --git a/client/assets/src/views/channel.js b/client/assets/src/views/channel.js
new file mode 100644 (file)
index 0000000..9136cee
--- /dev/null
@@ -0,0 +1,33 @@
+_kiwi.view.Channel = _kiwi.view.Panel.extend({
+    initialize: function (options) {
+        this.initializePanel(options);
+        this.model.bind('change:topic', this.topic, 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;">Joining channel.. <span class="loader"></span></div>');
+        }
+    },
+
+    // Override the existing newMsg() method to remove the joining channel loader
+    newMsg: function () {
+        this.$el.find('.initial_loader').slideUp(function () {
+            $(this).remove();
+        });
+
+        return this.constructor.__super__.newMsg.apply(this, arguments);
+    },
+
+    topic: function (topic) {
+        if (typeof topic !== 'string' || !topic) {
+            topic = this.model.get("topic");
+        }
+
+        this.model.addMsg('', '== Topic for ' + this.model.get('name') + ' is: ' + topic, 'topic');
+
+        // If this is the active channel then update the topic bar
+        if (_kiwi.app.panels().active === this) {
+            _kiwi.app.topicbar.setCurrentTopic(this.model.get("topic"));
+        }
+    }
+});
\ No newline at end of file
diff --git a/client/assets/src/views/controlbox.js b/client/assets/src/views/controlbox.js
new file mode 100644 (file)
index 0000000..58c18b4
--- /dev/null
@@ -0,0 +1,262 @@
+_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'));
+        });
+    },
+
+    showNickChange: function (ev) {
+        (new _kiwi.view.NickChangeBox()).render();
+    },
+
+    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):                     // tab
+            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;
+                });
+                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();
+
+                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 command, params,
+            pre_processed;
+        
+        // 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(' ');
+        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'));
+        }
+
+        // Trigger the command events
+        this.trigger('command', {command: command, params: params});
+        this.trigger('command:' + command, {command: command, params: 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 (!this._events['command:' + command]) {
+            this.trigger('unknown_command', {command: command, params: params});
+        }
+    },
+
+
+    addPluginIcon: function ($icon) {
+        var $tool = $('<div class="tool"></div>').append($icon);
+        this.$el.find('.input_tools').append($tool);
+        _kiwi.app.view.doLayout();
+    }
+});
\ No newline at end of file
diff --git a/client/assets/src/views/mediamessage.js b/client/assets/src/views/mediamessage.js
new file mode 100644 (file)
index 0000000..5a6d131
--- /dev/null
@@ -0,0 +1,120 @@
+_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');
+    },
+
+    // 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="icon-chevron-up"></i> Close media</a><br /><div class="content"></div></div>');
+            this.$content.find('.content').append(this.mediaTypes[this.$el.data('type')].apply(this, []) || 'Not found :(');
+        }
+
+        // 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>Loading tweet..</div>');
+        },
+
+
+        image: function () {
+            return $('<a href="' + this.url + '" target="_blank"><img height="100" src="' + this.url + '" /></a>');
+        },
+
+
+        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="icon-arrow-up"></i> <%- ups %> &nbsp;&nbsp; <i class="icon-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>Loading Reddit thread..</div>');
+        }
+    }
+
+}, {
+
+    // Build the closed media HTML from a URL
+    buildHtml: function (url) {
+        var html = '', matches;
+
+        // 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="icon-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="icon-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="icon-chevron-right"></i></a></span>';
+        }
+
+        return html;
+    }
+});
\ No newline at end of file
diff --git a/client/assets/src/views/member.js b/client/assets/src/views/member.js
new file mode 100644 (file)
index 0000000..0b42a09
--- /dev/null
@@ -0,0 +1,16 @@
+_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.addClass('mode ' + prefix_css_class);
+        $this.html('<a class="nick"><span class="prefix">' + this.model.get("prefix") + '</span>' + this.model.get("nick") + '</a>');
+        $this.data('member', this.model);
+        return this;
+    }
+});
\ No newline at end of file
diff --git a/client/assets/src/views/memberlist.js b/client/assets/src/views/memberlist.js
new file mode 100644 (file)
index 0000000..50f0cf6
--- /dev/null
@@ -0,0 +1,65 @@
+_kiwi.view.MemberList = Backbone.View.extend({
+    tagName: "ul",
+    events: {
+        "click .nick": "nickClick"
+    },
+    initialize: function (options) {
+        this.model.bind('all', this.render, this);
+        $(this.el).appendTo('#kiwi .memberlists');
+    },
+    render: function () {
+        var $this = this.$el;
+        $this.empty();
+        this.model.forEach(function (member) {
+            $this.append(member.view.el);
+        });
+        return this;
+    },
+    nickClick: function (event) {
+        var $target = $(event.currentTarget).parent('li'),
+            member = $target.data('member'),
+            userbox;
+
+        event.stopPropagation();
+
+        // If the userbox already exists here, hide it
+        if ($target.find('.userbox').length > 0) {
+            $('.userbox', this.$el).remove();
+            return;
+        }
+
+        userbox = new _kiwi.view.UserBox();
+        userbox.member = member;
+        userbox.channel = this.model.channel;
+
+        if (!this.model.getByNick(_kiwi.app.connections.active_connection.get('nick')).get('is_op')) {
+            userbox.$el.children('.if_op').remove();
+        }
+
+        var menu = new _kiwi.view.MenuBox(member.get('nick') || 'User');
+        menu.addItem('userbox', userbox.$el);
+        menu.show();
+
+        // Position the userbox + menubox
+        (function() {
+            var t = event.pageY,
+                m_bottom = t + menu.$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 - menu.$el.outerHeight();
+            }
+
+            // Set the new positon
+            menu.$el.offset({
+                left: _kiwi.app.view.$el.width() - menu.$el.outerWidth() - 20,
+                top: t
+            });
+        }).call(this);
+    },
+    show: function () {
+        $('#kiwi .memberlists').children().removeClass('active');
+        $(this.el).addClass('active');
+    }
+});
\ No newline at end of file
diff --git a/client/assets/src/views/menubox.js b/client/assets/src/views/menubox.js
new file mode 100644 (file)
index 0000000..143f64d
--- /dev/null
@@ -0,0 +1,98 @@
+_kiwi.view.MenuBox = Backbone.View.extend({
+    events: {
+        'click .ui_menu_foot .close': 'dispose'
+    },
+
+    initialize: function(title) {
+        var that = this;
+
+        this.$el = $('<div class="ui_menu"></div>');
+
+        this._title = title || '';
+        this._items = {};
+        this._display_footer = true;
+        this._close_on_blur = true;
+
+        this._close_proxy = function(event) {
+            that.onDocumentClick(event);
+        };
+        $(document).on('click', this._close_proxy);
+    },
+
+
+    render: function() {
+        var that = this;
+
+        this.$el.find('*').remove();
+
+        if (this._title) {
+            $('<div class="ui_menu_title"></div>')
+                .text(this._title)
+                .appendTo(this.$el);
+        }
+
+
+        _.each(this._items, function(item) {
+            var $item = $('<div class="ui_menu_content hover"></div>')
+                .append(item);
+
+            that.$el.append($item);
+        });
+
+        if (this._display_footer)
+            this.$el.append('<div class="ui_menu_foot"><a class="close" onclick="">Close <i class="icon-remove"></i></a></div>');
+    },
+
+
+    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();
+
+        $(document).off('click', this._close_proxy);
+    },
+
+
+    addItem: function(item_name, $item) {
+        $item = $($item);
+        if ($item.is('a')) $item.addClass('icon-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() {
+        this.render();
+        this.$el.appendTo(_kiwi.app.view.$el);
+    }
+});
\ No newline at end of file
diff --git a/client/assets/src/views/networktabs.js b/client/assets/src/views/networktabs.js
new file mode 100644 (file)
index 0000000..10ae2f5
--- /dev/null
@@ -0,0 +1,24 @@
+// 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 .tabs'));
+    },
+
+    networkAdded: function(network) {
+        $('<li class="connection"></li>')
+            .append(network.panels.view.$el)
+            .appendTo(this.$el);
+    },
+
+    networkRemoved: function(network) {
+        network.panels.view.remove();
+
+        _kiwi.app.view.doLayout();
+    }
+});
\ No newline at end of file
diff --git a/client/assets/src/views/nickchangebox.js b/client/assets/src/views/nickchangebox.js
new file mode 100644 (file)
index 0000000..8014ed5
--- /dev/null
@@ -0,0 +1,34 @@
+_kiwi.view.NickChangeBox = Backbone.View.extend({
+    events: {
+        'submit': 'changeNick',
+        'click .cancel': 'close'
+    },
+    
+    initialize: function () {
+        this.$el = $($('#tmpl_nickchange').html());
+    },
+    
+    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();
+
+    },
+
+    changeNick: function (event) {
+        var that = this;
+
+        event.preventDefault();
+
+        _kiwi.app.connections.active_connection.gateway.changeNick(this.$el.find('input').val(), function (err, val) {
+            that.close();
+        });
+        return false;
+    }
+});
\ No newline at end of file
diff --git a/client/assets/src/views/panel.js b/client/assets/src/views/panel.js
new file mode 100644 (file)
index 0000000..7200d00
--- /dev/null
@@ -0,0 +1,300 @@
+_kiwi.view.Panel = Backbone.View.extend({
+    tagName: "div",
+    className: "panel messages",
+
+    events: {
+        "click .chan": "chanClick",
+        'click .media .open': 'mediaClick',
+        'mouseenter .msg .nick': 'msgEnter',
+        'mouseleave .msg .nick': 'msgLeave'
+    },
+
+    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.bind('msg', this.newMsg, this);
+        this.msg_count = 0;
+
+        this.model.set({"view": this}, {"silent": true});
+    },
+
+    render: function () {
+        var that = this;
+
+        this.$el.empty();
+        _.each(this.model.get('scrollback'), function (msg) {
+            that.newMsg(msg);
+        });
+    },
+
+    newMsg: function (msg) {
+        var re, line_msg, $this = this.$el,
+            nick_colour_hex, nick_hex, is_highlight, msg_css_classes = '';
+
+        // Nick highlight detecting
+        if ((new RegExp('\\b' + _kiwi.app.connections.active_connection.get('nick') + '\\b', 'i')).test(msg.msg)) {
+            is_highlight = true;
+            msg_css_classes += ' highlight';
+        }
+
+        // Escape any HTML that may be in here
+        msg.msg =  $('<div />').text(msg.msg).html();
+
+        // Make the channels clickable
+        re = new RegExp('(?:^|\\s)([' + _kiwi.gateway.get('channel_prefix') + '][^ ,.\\007]+)', 'g');
+        msg.msg = msg.msg.replace(re, function (match) {
+            return '<a class="chan" data-channel="' + match.trim() + '">' + match + '</a>';
+        });
+
+
+        // Parse any links found
+        msg.msg = msg.msg.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 = '';
+
+            // 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 + '">' + nice + '</a> ' + extra_html;
+        });
+
+
+        // Convert IRC formatting into HTML formatting
+        msg.msg = formatIRCMsg(msg.msg);
+
+
+        // Add some colours to the nick (Method based on IRSSIs nickcolor.pl)
+        nick_colour_hex = (function (nick) {
+            var nick_int = 0, rgb;
+
+            _.map(nick.split(''), function (i) { nick_int += i.charCodeAt(0); });
+            rgb = hsl2rgb(nick_int % 255, 70, 35);
+            rgb = rgb[2] | (rgb[1] << 8) | (rgb[0] << 16);
+
+            return '#' + rgb.toString(16);
+        })(msg.nick);
+
+        msg.nick_style = 'color:' + nick_colour_hex + ';';
+
+        // Generate a hex string from the nick to be used as a CSS class name
+        nick_hex = msg.nick_css_class = '';
+        if (msg.nick) {
+            _.map(msg.nick.split(''), function (char) {
+                nick_hex += char.charCodeAt(0).toString(16);
+            });
+            msg_css_classes += ' nick_' + nick_hex;
+        }
+
+        // Build up and add the line
+        msg.msg_css_classes = msg_css_classes;
+        line_msg = '<div class="msg <%= type %> <%= msg_css_classes %>"><div class="time"><%- time %></div><div class="nick" style="<%= nick_style %>"><%- nick %></div><div class="text" style="<%= style %>"><%= msg %> </div></div>';
+        $this.append(_.template(line_msg, msg));
+
+        // Activity/alerts based on the type of new message
+        if (msg.type.match(/^action /)) {
+            this.alert('action');
+
+        } else if (is_highlight) {
+            _kiwi.app.view.alertWindow('* People are talking!');
+            _kiwi.app.view.playSound('highlight');
+            this.alert('highlight');
+
+        } else {
+            // If this is the active panel, send an alert out
+            if (this.model.isActive()) {
+                _kiwi.app.view.alertWindow('* People are talking!');
+            }
+            this.alert('activity');
+        }
+
+        if (this.model.isQuery() && !this.model.isActive()) {
+            _kiwi.app.view.alertWindow('* People are talking!');
+            _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 $act = this.model.tab.find('.activity');
+            $act.text((parseInt($act.text(), 10) || 0) + 1);
+            if ($act.text() === '0') {
+                $act.addClass('zero');
+            } else {
+                $act.removeClass('zero');
+            }
+        }).apply(this);
+
+        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.$el).remove();
+            this.msg_count--;
+        }
+    },
+    chanClick: function (event) {
+        if (event.target) {
+            _kiwi.gateway.join(null, $(event.target).data('channel'));
+        } else {
+            // IE...
+            _kiwi.gateway.join(null, $(event.srcElement).data('channel'));
+        }
+    },
+
+    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]});
+            $media.data('media', media_message);
+        }
+
+        $media.data('media', media_message);
+
+        media_message.open();
+    },
+
+    // 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');
+    },
+
+    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 .memberlists').removeClass('disabled');
+            members.view.show();
+        } else {
+            // Memberlist not found for this panel, hide any active ones
+            $('#kiwi .memberlists').addClass('disabled').children().removeClass('active');
+        }
+
+        // Remove any alerts and activity counters for this panel
+        this.alert('none');
+        this.model.tab.find('.activity').text('0').addClass('zero');
+
+        _kiwi.app.panels.trigger('active', this.model, _kiwi.app.panels().active);
+        this.model.trigger('active', this.model);
+
+        _kiwi.app.view.doLayout();
+
+        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;
+        }
+    }
+});
\ No newline at end of file
diff --git a/client/assets/src/views/resizehandler.js b/client/assets/src/views/resizehandler.js
new file mode 100644 (file)
index 0000000..d03455c
--- /dev/null
@@ -0,0 +1,29 @@
+_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;
+
+        this.$el.css('left', event.clientX - (this.$el.outerWidth(true) / 2));
+        $('#kiwi .memberlists').css('width', this.$el.parent().width() - (this.$el.position().left + this.$el.outerWidth()));
+        _kiwi.app.view.doLayout();
+    }
+});
\ No newline at end of file
diff --git a/client/assets/src/views/serverselect.js b/client/assets/src/views/serverselect.js
new file mode 100644 (file)
index 0000000..6fa8e9b
--- /dev/null
@@ -0,0 +1,242 @@
+_kiwi.view.ServerSelect = function () {
+    // Are currently showing all the controlls or just a nick_change box?
+    var state = 'all';
+
+    var model = Backbone.View.extend({
+        events: {
+            'submit form': 'submitForm',
+            'click .show_more': 'showMore',
+            'change .have_pass input': 'showPass',
+            'change .have_key input': 'showKey',
+            'click .icon-key': 'channelKeyIconClick'
+        },
+
+        initialize: function () {
+            var that = this;
+
+            this.$el = $($('#tmpl_server_select').html());
+
+            // 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');
+                }
+            }
+
+            _kiwi.gateway.bind('onconnect', this.networkConnected, this);
+            _kiwi.gateway.bind('connecting', this.networkConnecting, this);
+            _kiwi.gateway.bind('onirc_error', this.onIrcError, this);
+        },
+
+        dispose: function() {
+            _kiwi.gateway.off('onconnect', this.networkConnected, this);
+            _kiwi.gateway.off('connecting', this.networkConnecting, this);
+            _kiwi.gateway.off('onirc_error', this.onIrcError, this);
+
+            this.$el.remove();
+        },
+
+        submitForm: function (event) {
+            event.preventDefault();
+
+            // Make sure a nick is chosen
+            if (!$('input.nick', this.$el).val().trim()) {
+                this.setStatus('Select a nickname first!');
+                $('input.nick', this.$el).select();
+                return;
+            }
+
+            if (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()
+            };
+
+            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) {
+            $('.more', this.$el).slideDown('fast');
+            $('input.server', this.$el).select();
+        },
+
+        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();
+            }
+        },
+
+        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();
+            }
+
+            state = new_state;
+        },
+
+        infoBoxShow: function() {
+            var $side_panel = this.$el.find('.side_panel');
+            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();
+        },
+
+        networkConnected: function (event) {
+            this.setStatus('Connected :)', 'ok');
+            $('form', this.$el).hide();
+        },
+
+        networkConnecting: function (event) {
+            this.setStatus('Connecting..', 'ok');
+        },
+
+        onIrcError: function (data) {
+            $('button', this.$el).attr('disabled', null);
+
+            if (data.error == 'nickname_in_use') {
+                this.setStatus('Nickname already taken');
+                this.show('nick_change');
+            }
+
+            if (data.error == 'password_mismatch') {
+                this.setStatus('Incorrect Password');
+                this.show('nick_change');
+                that.$el.find('.password').select();
+            }
+        },
+
+        showError: function (error_reason) {
+            var err_text = 'Error Connecting';
+
+            if (error_reason) {
+                switch (error_reason) {
+                case 'ENOTFOUND':
+                    err_text = 'Server not found';
+                    break;
+
+                case 'ECONNREFUSED':
+                    err_text += ' (Connection refused)';
+                    break;
+
+                default:
+                    err_text += ' (' + error_reason + ')';
+                }
+            }
+
+            this.setStatus(err_text, 'error');
+            $('button', this.$el).attr('disabled', null);
+            this.show();
+        }
+    });
+
+
+    return new model(arguments);
+};
\ No newline at end of file
diff --git a/client/assets/src/views/statusmessage.js b/client/assets/src/views/statusmessage.js
new file mode 100644 (file)
index 0000000..fdd30f5
--- /dev/null
@@ -0,0 +1,42 @@
+_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(text).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);
+    }
+});
\ No newline at end of file
diff --git a/client/assets/src/views/tabs.js b/client/assets/src/views/tabs.js
new file mode 100644 (file)
index 0000000..74b5a95
--- /dev/null
@@ -0,0 +1,131 @@
+// 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);
+        }
+    },
+
+    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>' + (panel.get('title') || panel.get('name')) + '</span><div class="activity"></div></li>');
+
+        if (panel.isServer()) {
+            panel.tab.addClass('server');
+            panel.tab.addClass('icon-nonexistant');
+        }
+
+        panel.tab.data('panel', panel);
+
+        if (this.is_network)
+            panel.tab.data('connection_id', this.model.network.get('connection_id'));
+
+        panel.tab.appendTo(this.$el);
+
+        panel.bind('change:title', this.updateTabTitle);
+        panel.bind('change:name', this.updateTabTitle);
+
+        _kiwi.app.view.doLayout();
+    },
+    panelRemoved: function (panel) {
+        panel.tab.remove();
+        delete panel.tab;
+
+        _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');
+
+        // Only show the part image on non-server tabs
+        if (!panel.isServer()) {
+            panel.tab.append('<span class="part icon-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;
+
+        // Only need to part if it's a channel
+        // If the nicklist is empty, we haven't joined the channel as yet
+        if (panel.isChannel() && panel.get('members').models.length > 0) {
+            this.model.network.gateway.part(panel.get('name'));
+        } else {
+            panel.close();
+        }
+    }
+});
\ No newline at end of file
diff --git a/client/assets/src/views/topicbar.js b/client/assets/src/views/topicbar.js
new file mode 100644 (file)
index 0000000..7cc2f37
--- /dev/null
@@ -0,0 +1,43 @@
+_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.setCurrentTopic(active_panel.get('topic') || '');
+                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.gateway.topic(null, _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)));
+    }
+});
\ No newline at end of file
diff --git a/client/assets/src/views/userbox.js b/client/assets/src/views/userbox.js
new file mode 100644 (file)
index 0000000..5461f83
--- /dev/null
@@ -0,0 +1,57 @@
+_kiwi.view.UserBox = Backbone.View.extend({
+    events: {
+        'click .query': 'queryClick',
+        'click .info': 'infoClick',
+        'click .slap': 'slapClick',
+        'click .op': 'opClick',
+        'click .deop': 'deopClick',
+        'click .voice': 'voiceClick',
+        'click .devoice': 'devoiceClick',
+        'click .kick': 'kickClick',
+        'click .ban': 'banClick'
+    },
+
+    initialize: function () {
+        this.$el = $($('#tmpl_userbox').html());
+    },
+
+    queryClick: function (event) {
+        var panel = new _kiwi.model.Query({name: this.member.get('nick')});
+        _kiwi.app.connections.active_connection.panels.add(panel);
+        panel.view.show();
+    },
+
+    infoClick: function (event) {
+        _kiwi.app.controlbox.processInput('/whois ' + this.member.get('nick'));
+    },
+
+    slapClick: function (event) {
+        _kiwi.app.controlbox.processInput('/slap ' + this.member.get('nick'));
+    },
+
+    opClick: function (event) {
+        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +o ' + this.member.get('nick'));
+    },
+
+    deopClick: function (event) {
+        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' -o ' + this.member.get('nick'));
+    },
+
+    voiceClick: function (event) {
+        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +v ' + this.member.get('nick'));
+    },
+
+    devoiceClick: function (event) {
+        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' -v ' + this.member.get('nick'));
+    },
+
+    kickClick: function (event) {
+        // TODO: Enable the use of a custom kick message
+        _kiwi.app.controlbox.processInput('/kick ' + this.member.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.member.get('nick') + '!*');
+    }
+});
\ No newline at end of file
diff --git a/client/assets/src/views/view.js b/client/assets/src/views/view.js
deleted file mode 100644 (file)
index 11b2f9f..0000000
+++ /dev/null
@@ -1,1852 +0,0 @@
-/*jslint white:true, regexp: true, nomen: true, devel: true, undef: true, browser: true, continue: true, sloppy: true, forin: true, newcap: true, plusplus: true, maxerr: 50, indent: 4 */\r
-/*global kiwi */\r
-_kiwi.view.Member = Backbone.View.extend({\r
-    tagName: "li",\r
-    initialize: function (options) {\r
-        this.model.bind('change', this.render, this);\r
-        this.render();\r
-    },\r
-    render: function () {\r
-        var $this = this.$el,\r
-            prefix_css_class = (this.model.get('modes') || []).join(' ');\r
-\r
-        $this.addClass('mode ' + prefix_css_class);\r
-        $this.html('<a class="nick"><span class="prefix">' + this.model.get("prefix") + '</span>' + this.model.get("nick") + '</a>');\r
-        $this.data('member', this.model);\r
-        return this;\r
-    }\r
-});\r
-\r
-_kiwi.view.MemberList = Backbone.View.extend({\r
-    tagName: "ul",\r
-    events: {\r
-        "click .nick": "nickClick"\r
-    },\r
-    initialize: function (options) {\r
-        this.model.bind('all', this.render, this);\r
-        $(this.el).appendTo('#kiwi .memberlists');\r
-    },\r
-    render: function () {\r
-        var $this = this.$el;\r
-        $this.empty();\r
-        this.model.forEach(function (member) {\r
-            $this.append(member.view.el);\r
-        });\r
-        return this;\r
-    },\r
-    nickClick: function (event) {\r
-        var $target = $(event.currentTarget).parent('li'),\r
-            member = $target.data('member'),\r
-            userbox;\r
-\r
-        event.stopPropagation();\r
-\r
-        // If the userbox already exists here, hide it\r
-        if ($target.find('.userbox').length > 0) {\r
-            $('.userbox', this.$el).remove();\r
-            return;\r
-        }\r
-\r
-        userbox = new _kiwi.view.UserBox();\r
-        userbox.member = member;\r
-        userbox.channel = this.model.channel;\r
-\r
-        if (!this.model.getByNick(_kiwi.app.connections.active_connection.get('nick')).get('is_op')) {\r
-            userbox.$el.children('.if_op').remove();\r
-        }\r
-\r
-        var menu = new _kiwi.view.MenuBox(member.get('nick') || 'User');\r
-        menu.addItem('userbox', userbox.$el);\r
-        menu.show();\r
-\r
-        // Position the userbox + menubox\r
-        (function() {\r
-            var t = event.pageY,\r
-                m_bottom = t + menu.$el.outerHeight(),  // Where the bottom of menu will be\r
-                memberlist_bottom = this.$el.parent().offset().top + this.$el.parent().outerHeight();\r
-\r
-            // If the bottom of the userbox is going to be too low.. raise it\r
-            if (m_bottom > memberlist_bottom){\r
-                t = memberlist_bottom - menu.$el.outerHeight();\r
-            }\r
-\r
-            // Set the new positon\r
-            menu.$el.offset({\r
-                left: _kiwi.app.view.$el.width() - menu.$el.outerWidth() - 20,\r
-                top: t\r
-            });\r
-        }).call(this);\r
-    },\r
-    show: function () {\r
-        $('#kiwi .memberlists').children().removeClass('active');\r
-        $(this.el).addClass('active');\r
-    }\r
-});\r
-\r
-\r
-\r
-_kiwi.view.UserBox = Backbone.View.extend({\r
-    events: {\r
-        'click .query': 'queryClick',\r
-        'click .info': 'infoClick',\r
-        'click .slap': 'slapClick',\r
-        'click .op': 'opClick',\r
-        'click .deop': 'deopClick',\r
-        'click .voice': 'voiceClick',\r
-        'click .devoice': 'devoiceClick',\r
-        'click .kick': 'kickClick',\r
-        'click .ban': 'banClick'\r
-    },\r
-\r
-    initialize: function () {\r
-        this.$el = $($('#tmpl_userbox').html());\r
-    },\r
-\r
-    queryClick: function (event) {\r
-        var panel = new _kiwi.model.Query({name: this.member.get('nick')});\r
-        _kiwi.app.connections.active_connection.panels.add(panel);\r
-        panel.view.show();\r
-    },\r
-\r
-    infoClick: function (event) {\r
-        _kiwi.app.controlbox.processInput('/whois ' + this.member.get('nick'));\r
-    },\r
-\r
-    slapClick: function (event) {\r
-        _kiwi.app.controlbox.processInput('/slap ' + this.member.get('nick'));\r
-    },\r
-\r
-    opClick: function (event) {\r
-        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +o ' + this.member.get('nick'));\r
-    },\r
-\r
-    deopClick: function (event) {\r
-        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' -o ' + this.member.get('nick'));\r
-    },\r
-\r
-    voiceClick: function (event) {\r
-        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +v ' + this.member.get('nick'));\r
-    },\r
-\r
-    devoiceClick: function (event) {\r
-        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' -v ' + this.member.get('nick'));\r
-    },\r
-\r
-    kickClick: function (event) {\r
-        // TODO: Enable the use of a custom kick message\r
-        _kiwi.app.controlbox.processInput('/kick ' + this.member.get('nick') + ' Bye!');\r
-    },\r
-\r
-    banClick: function (event) {\r
-        // TODO: Set ban on host, not just on nick\r
-        _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +b ' + this.member.get('nick') + '!*');\r
-    }\r
-});\r
-\r
-_kiwi.view.NickChangeBox = Backbone.View.extend({\r
-    events: {\r
-        'submit': 'changeNick',\r
-        'click .cancel': 'close'\r
-    },\r
-    \r
-    initialize: function () {\r
-        this.$el = $($('#tmpl_nickchange').html());\r
-    },\r
-    \r
-    render: function () {\r
-        // Add the UI component and give it focus\r
-        _kiwi.app.controlbox.$el.prepend(this.$el);\r
-        this.$el.find('input').focus();\r
-\r
-        this.$el.css('bottom', _kiwi.app.controlbox.$el.outerHeight(true));\r
-    },\r
-    \r
-    close: function () {\r
-        this.$el.remove();\r
-\r
-    },\r
-\r
-    changeNick: function (event) {\r
-        var that = this;\r
-\r
-        event.preventDefault();\r
-\r
-        _kiwi.app.connections.active_connection.gateway.changeNick(this.$el.find('input').val(), function (err, val) {\r
-            that.close();\r
-        });\r
-        return false;\r
-    }\r
-});\r
-\r
-_kiwi.view.ServerSelect = function () {\r
-    // Are currently showing all the controlls or just a nick_change box?\r
-    var state = 'all';\r
-\r
-    var model = Backbone.View.extend({\r
-        events: {\r
-            'submit form': 'submitForm',\r
-            'click .show_more': 'showMore',\r
-            'change .have_pass input': 'showPass',\r
-            'change .have_key input': 'showKey',\r
-            'click .icon-key': 'channelKeyIconClick'\r
-        },\r
-\r
-        initialize: function () {\r
-            var that = this;\r
-\r
-            this.$el = $($('#tmpl_server_select').html());\r
-\r
-            // Remove the 'more' link if the server has disabled server changing\r
-            if (_kiwi.app.server_settings && _kiwi.app.server_settings.connection) {\r
-                if (!_kiwi.app.server_settings.connection.allow_change) {\r
-                    this.$el.find('.show_more').remove();\r
-                    this.$el.addClass('single_server');\r
-                }\r
-            }\r
-\r
-            _kiwi.gateway.bind('onconnect', this.networkConnected, this);\r
-            _kiwi.gateway.bind('connecting', this.networkConnecting, this);\r
-            _kiwi.gateway.bind('onirc_error', this.onIrcError, this);\r
-        },\r
-\r
-        dispose: function() {\r
-            _kiwi.gateway.off('onconnect', this.networkConnected, this);\r
-            _kiwi.gateway.off('connecting', this.networkConnecting, this);\r
-            _kiwi.gateway.off('onirc_error', this.onIrcError, this);\r
-\r
-            this.$el.remove();\r
-        },\r
-\r
-        submitForm: function (event) {\r
-            event.preventDefault();\r
-\r
-            // Make sure a nick is chosen\r
-            if (!$('input.nick', this.$el).val().trim()) {\r
-                this.setStatus('Select a nickname first!');\r
-                $('input.nick', this.$el).select();\r
-                return;\r
-            }\r
-\r
-            if (state === 'nick_change') {\r
-                this.submitNickChange(event);\r
-            } else {\r
-                this.submitLogin(event);\r
-            }\r
-\r
-            $('button', this.$el).attr('disabled', 1);\r
-            return;\r
-        },\r
-\r
-        submitLogin: function (event) {\r
-            // If submitting is disabled, don't do anything\r
-            if ($('button', this.$el).attr('disabled')) return;\r
-\r
-            var values = {\r
-                nick: $('input.nick', this.$el).val(),\r
-                server: $('input.server', this.$el).val(),\r
-                port: $('input.port', this.$el).val(),\r
-                ssl: $('input.ssl', this.$el).prop('checked'),\r
-                password: $('input.password', this.$el).val(),\r
-                channel: $('input.channel', this.$el).val(),\r
-                channel_key: $('input.channel_key', this.$el).val()\r
-            };\r
-\r
-            this.trigger('server_connect', values);\r
-        },\r
-\r
-        submitNickChange: function (event) {\r
-            _kiwi.gateway.changeNick(null, $('input.nick', this.$el).val());\r
-            this.networkConnecting();\r
-        },\r
-\r
-        showPass: function (event) {\r
-            if (this.$el.find('tr.have_pass input').is(':checked')) {\r
-                this.$el.find('tr.pass').show().find('input').focus();\r
-            } else {\r
-                this.$el.find('tr.pass').hide().find('input').val('');\r
-            }\r
-        },\r
-\r
-        channelKeyIconClick: function (event) {\r
-            this.$el.find('tr.have_key input').click();\r
-        },\r
-\r
-        showKey: function (event) {\r
-            if (this.$el.find('tr.have_key input').is(':checked')) {\r
-                this.$el.find('tr.key').show().find('input').focus();\r
-            } else {\r
-                this.$el.find('tr.key').hide().find('input').val('');\r
-            }\r
-        },\r
-\r
-        showMore: function (event) {\r
-            $('.more', this.$el).slideDown('fast');\r
-            $('input.server', this.$el).select();\r
-        },\r
-\r
-        populateFields: function (defaults) {\r
-            var nick, server, port, channel, channel_key, ssl, password;\r
-\r
-            defaults = defaults || {};\r
-\r
-            nick = defaults.nick || '';\r
-            server = defaults.server || '';\r
-            port = defaults.port || 6667;\r
-            ssl = defaults.ssl || 0;\r
-            password = defaults.password || '';\r
-            channel = defaults.channel || '';\r
-            channel_key = defaults.channel_key || '';\r
-\r
-            $('input.nick', this.$el).val(nick);\r
-            $('input.server', this.$el).val(server);\r
-            $('input.port', this.$el).val(port);\r
-            $('input.ssl', this.$el).prop('checked', ssl);\r
-            $('input#server_select_show_pass', this.$el).prop('checked', !(!password));\r
-            $('input.password', this.$el).val(password);\r
-            if (!(!password)) {\r
-                $('tr.pass', this.$el).show();\r
-            }\r
-            $('input.channel', this.$el).val(channel);\r
-            $('input#server_select_show_channel_key', this.$el).prop('checked', !(!channel_key));\r
-            $('input.channel_key', this.$el).val(channel_key);\r
-            if (!(!channel_key)) {\r
-                $('tr.key', this.$el).show();\r
-            }\r
-        },\r
-\r
-        hide: function () {\r
-            this.$el.slideUp();\r
-        },\r
-\r
-        show: function (new_state) {\r
-            new_state = new_state || 'all';\r
-\r
-            this.$el.show();\r
-\r
-            if (new_state === 'all') {\r
-                $('.show_more', this.$el).show();\r
-\r
-            } else if (new_state === 'more') {\r
-                $('.more', this.$el).slideDown('fast');\r
-\r
-            } else if (new_state === 'nick_change') {\r
-                $('.more', this.$el).hide();\r
-                $('.show_more', this.$el).hide();\r
-                $('input.nick', this.$el).select();\r
-            }\r
-\r
-            state = new_state;\r
-        },\r
-\r
-        infoBoxShow: function() {\r
-            var $side_panel = this.$el.find('.side_panel');\r
-            this.$el.animate({\r
-                width: parseInt($side_panel.css('left'), 10) + $side_panel.find('.content:first').outerWidth()\r
-            });\r
-        },\r
-\r
-        infoBoxHide: function() {\r
-            var $side_panel = this.$el.find('.side_panel');\r
-            this.$el.animate({\r
-                width: parseInt($side_panel.css('left'), 10)\r
-            });\r
-        },\r
-\r
-        infoBoxSet: function($info_view) {\r
-            this.$el.find('.side_panel .content')\r
-                .empty()\r
-                .append($info_view);\r
-        },\r
-\r
-        setStatus: function (text, class_name) {\r
-            $('.status', this.$el)\r
-                .text(text)\r
-                .attr('class', 'status')\r
-                .addClass(class_name||'')\r
-                .show();\r
-        },\r
-        clearStatus: function () {\r
-            $('.status', this.$el).hide();\r
-        },\r
-\r
-        networkConnected: function (event) {\r
-            this.setStatus('Connected :)', 'ok');\r
-            $('form', this.$el).hide();\r
-        },\r
-\r
-        networkConnecting: function (event) {\r
-            this.setStatus('Connecting..', 'ok');\r
-        },\r
-\r
-        onIrcError: function (data) {\r
-            $('button', this.$el).attr('disabled', null);\r
-\r
-            if (data.error == 'nickname_in_use') {\r
-                this.setStatus('Nickname already taken');\r
-                this.show('nick_change');\r
-            }\r
-\r
-            if (data.error == 'password_mismatch') {\r
-                this.setStatus('Incorrect Password');\r
-                this.show('nick_change');\r
-                that.$el.find('.password').select();\r
-            }\r
-        },\r
-\r
-        showError: function (error_reason) {\r
-            var err_text = 'Error Connecting';\r
-\r
-            if (error_reason) {\r
-                switch (error_reason) {\r
-                case 'ENOTFOUND':\r
-                    err_text = 'Server not found';\r
-                    break;\r
-\r
-                case 'ECONNREFUSED':\r
-                    err_text += ' (Connection refused)';\r
-                    break;\r
-\r
-                default:\r
-                    err_text += ' (' + error_reason + ')';\r
-                }\r
-            }\r
-\r
-            this.setStatus(err_text, 'error');\r
-            $('button', this.$el).attr('disabled', null);\r
-            this.show();\r
-        }\r
-    });\r
-\r
-\r
-    return new model(arguments);\r
-};\r
-\r
-\r
-_kiwi.view.Panel = Backbone.View.extend({\r
-    tagName: "div",\r
-    className: "panel messages",\r
-\r
-    events: {\r
-        "click .chan": "chanClick",\r
-        'click .media .open': 'mediaClick',\r
-        'mouseenter .msg .nick': 'msgEnter',\r
-        'mouseleave .msg .nick': 'msgLeave'\r
-    },\r
-\r
-    initialize: function (options) {\r
-        this.initializePanel(options);\r
-    },\r
-\r
-    initializePanel: function (options) {\r
-        this.$el.css('display', 'none');\r
-        options = options || {};\r
-\r
-        // Containing element for this panel\r
-        if (options.container) {\r
-            this.$container = $(options.container);\r
-        } else {\r
-            this.$container = $('#kiwi .panels .container1');\r
-        }\r
-\r
-        this.$el.appendTo(this.$container);\r
-\r
-        this.alert_level = 0;\r
-\r
-        this.model.bind('msg', this.newMsg, this);\r
-        this.msg_count = 0;\r
-\r
-        this.model.set({"view": this}, {"silent": true});\r
-    },\r
-\r
-    render: function () {\r
-        var that = this;\r
-\r
-        this.$el.empty();\r
-        _.each(this.model.get('scrollback'), function (msg) {\r
-            that.newMsg(msg);\r
-        });\r
-    },\r
-\r
-    newMsg: function (msg) {\r
-        var re, line_msg, $this = this.$el,\r
-            nick_colour_hex, nick_hex, is_highlight, msg_css_classes = '';\r
-\r
-        // Nick highlight detecting\r
-        if ((new RegExp('\\b' + _kiwi.app.connections.active_connection.get('nick') + '\\b', 'i')).test(msg.msg)) {\r
-            is_highlight = true;\r
-            msg_css_classes += ' highlight';\r
-        }\r
-\r
-        // Escape any HTML that may be in here\r
-        msg.msg =  $('<div />').text(msg.msg).html();\r
-\r
-        // Make the channels clickable\r
-        re = new RegExp('(?:^|\\s)([' + _kiwi.gateway.get('channel_prefix') + '][^ ,.\\007]+)', 'g');\r
-        msg.msg = msg.msg.replace(re, function (match) {\r
-            return '<a class="chan" data-channel="' + match.trim() + '">' + match + '</a>';\r
-        });\r
-\r
-\r
-        // Parse any links found\r
-        msg.msg = msg.msg.replace(/(([A-Za-z][A-Za-z0-9\-]*\:\/\/)|(www\.))([\w.\-]+)([a-zA-Z]{2,6})(:[0-9]+)?(\/[\w#!:.?$'()[\]*,;~+=&%@!\-\/]*)?/gi, function (url) {\r
-            var nice = url,\r
-                extra_html = '';\r
-\r
-            // Add the http if no protoocol was found\r
-            if (url.match(/^www\./)) {\r
-                url = 'http://' + url;\r
-            }\r
-\r
-            // Shorten the displayed URL if it's going to be too long\r
-            if (nice.length > 100) {\r
-                nice = nice.substr(0, 100) + '...';\r
-            }\r
-\r
-            // Get any media HTML if supported\r
-            extra_html = _kiwi.view.MediaMessage.buildHtml(url);\r
-\r
-            // Make the link clickable\r
-            return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url + '">' + nice + '</a> ' + extra_html;\r
-        });\r
-\r
-\r
-        // Convert IRC formatting into HTML formatting\r
-        msg.msg = formatIRCMsg(msg.msg);\r
-\r
-\r
-        // Add some colours to the nick (Method based on IRSSIs nickcolor.pl)\r
-        nick_colour_hex = (function (nick) {\r
-            var nick_int = 0, rgb;\r
-\r
-            _.map(nick.split(''), function (i) { nick_int += i.charCodeAt(0); });\r
-            rgb = hsl2rgb(nick_int % 255, 70, 35);\r
-            rgb = rgb[2] | (rgb[1] << 8) | (rgb[0] << 16);\r
-\r
-            return '#' + rgb.toString(16);\r
-        })(msg.nick);\r
-\r
-        msg.nick_style = 'color:' + nick_colour_hex + ';';\r
-\r
-        // Generate a hex string from the nick to be used as a CSS class name\r
-        nick_hex = msg.nick_css_class = '';\r
-        if (msg.nick) {\r
-            _.map(msg.nick.split(''), function (char) {\r
-                nick_hex += char.charCodeAt(0).toString(16);\r
-            });\r
-            msg_css_classes += ' nick_' + nick_hex;\r
-        }\r
-\r
-        // Build up and add the line\r
-        msg.msg_css_classes = msg_css_classes;\r
-        line_msg = '<div class="msg <%= type %> <%= msg_css_classes %>"><div class="time"><%- time %></div><div class="nick" style="<%= nick_style %>"><%- nick %></div><div class="text" style="<%= style %>"><%= msg %> </div></div>';\r
-        $this.append(_.template(line_msg, msg));\r
-\r
-        // Activity/alerts based on the type of new message\r
-        if (msg.type.match(/^action /)) {\r
-            this.alert('action');\r
-\r
-        } else if (is_highlight) {\r
-            _kiwi.app.view.alertWindow('* People are talking!');\r
-            _kiwi.app.view.playSound('highlight');\r
-            this.alert('highlight');\r
-\r
-        } else {\r
-            // If this is the active panel, send an alert out\r
-            if (this.model.isActive()) {\r
-                _kiwi.app.view.alertWindow('* People are talking!');\r
-            }\r
-            this.alert('activity');\r
-        }\r
-\r
-        if (this.model.isQuery() && !this.model.isActive()) {\r
-            _kiwi.app.view.alertWindow('* People are talking!');\r
-            _kiwi.app.view.playSound('highlight');\r
-        }\r
-\r
-        // Update the activity counters\r
-        (function () {\r
-            // Only inrement the counters if we're not the active panel\r
-            if (this.model.isActive()) return;\r
-\r
-            var $act = this.model.tab.find('.activity');\r
-            $act.text((parseInt($act.text(), 10) || 0) + 1);\r
-            if ($act.text() === '0') {\r
-                $act.addClass('zero');\r
-            } else {\r
-                $act.removeClass('zero');\r
-            }\r
-        }).apply(this);\r
-\r
-        this.scrollToBottom();\r
-\r
-        // Make sure our DOM isn't getting too large (Acts as scrollback)\r
-        this.msg_count++;\r
-        if (this.msg_count > (parseInt(_kiwi.global.settings.get('scrollback'), 10) || 250)) {\r
-            $('.msg:first', this.$el).remove();\r
-            this.msg_count--;\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
-    mediaClick: function (event) {\r
-        var $media = $(event.target).parents('.media');\r
-        var media_message;\r
-\r
-        if ($media.data('media')) {\r
-            media_message = $media.data('media');\r
-        } else {\r
-            media_message = new _kiwi.view.MediaMessage({el: $media[0]});\r
-            $media.data('media', media_message);\r
-        }\r
-\r
-        $media.data('media', media_message);\r
-\r
-        media_message.open();\r
-    },\r
-\r
-    // Cursor hovers over a message\r
-    msgEnter: function (event) {\r
-        var nick_class;\r
-\r
-        // Find a valid class that this element has\r
-        _.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) {\r
-            if (css_class.match(/^nick_[a-z0-9]+/i)) {\r
-                nick_class = css_class;\r
-            }\r
-        });\r
-\r
-        // If no class was found..\r
-        if (!nick_class) return;\r
-\r
-        $('.'+nick_class).addClass('global_nick_highlight');\r
-    },\r
-\r
-    // Cursor leaves message\r
-    msgLeave: function (event) {\r
-        var nick_class;\r
-\r
-        // Find a valid class that this element has\r
-        _.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) {\r
-            if (css_class.match(/^nick_[a-z0-9]+/i)) {\r
-                nick_class = css_class;\r
-            }\r
-        });\r
-\r
-        // If no class was found..\r
-        if (!nick_class) return;\r
-\r
-        $('.'+nick_class).removeClass('global_nick_highlight');\r
-    },\r
-\r
-    show: function () {\r
-        var $this = this.$el;\r
-\r
-        // Hide all other panels and show this one\r
-        this.$container.children('.panel').css('display', 'none');\r
-        $this.css('display', 'block');\r
-\r
-        // Show this panels memberlist\r
-        var members = this.model.get("members");\r
-        if (members) {\r
-            $('#kiwi .memberlists').removeClass('disabled');\r
-            members.view.show();\r
-        } else {\r
-            // Memberlist not found for this panel, hide any active ones\r
-            $('#kiwi .memberlists').addClass('disabled').children().removeClass('active');\r
-        }\r
-\r
-        // Remove any alerts and activity counters for this panel\r
-        this.alert('none');\r
-        this.model.tab.find('.activity').text('0').addClass('zero');\r
-\r
-        _kiwi.app.panels.trigger('active', this.model, _kiwi.app.panels().active);\r
-        this.model.trigger('active', this.model);\r
-\r
-        _kiwi.app.view.doLayout();\r
-\r
-        this.scrollToBottom(true);\r
-    },\r
-\r
-\r
-    alert: function (level) {\r
-        // No need to highlight if this si the active panel\r
-        if (this.model == _kiwi.app.panels().active) return;\r
-\r
-        var types, type_idx;\r
-        types = ['none', 'action', 'activity', 'highlight'];\r
-\r
-        // Default alert level\r
-        level = level || 'none';\r
-\r
-        // If this alert level does not exist, assume clearing current level\r
-        type_idx = _.indexOf(types, level);\r
-        if (!type_idx) {\r
-            level = 'none';\r
-            type_idx = 0;\r
-        }\r
-\r
-        // Only 'upgrade' the alert. Never down (unless clearing)\r
-        if (type_idx !== 0 && type_idx <= this.alert_level) {\r
-            return;\r
-        }\r
-\r
-        // Clear any existing levels\r
-        this.model.tab.removeClass(function (i, css) {\r
-            return (css.match(/\balert_\S+/g) || []).join(' ');\r
-        });\r
-\r
-        // Add the new level if there is one\r
-        if (level !== 'none') {\r
-            this.model.tab.addClass('alert_' + level);\r
-        }\r
-\r
-        this.alert_level = type_idx;\r
-    },\r
-\r
-\r
-    // Scroll to the bottom of the panel\r
-    scrollToBottom: function (force_down) {\r
-        // If this isn't the active panel, don't scroll\r
-        if (this.model !== _kiwi.app.panels().active) return;\r
-\r
-        // Don't scroll down if we're scrolled up the panel a little\r
-        if (force_down || this.$container.scrollTop() + this.$container.height() > this.$el.outerHeight() - 150) {\r
-            this.$container[0].scrollTop = this.$container[0].scrollHeight;\r
-        }\r
-    }\r
-});\r
-\r
-_kiwi.view.Applet = _kiwi.view.Panel.extend({\r
-    className: 'panel applet',\r
-    initialize: function (options) {\r
-        this.initializePanel(options);\r
-    }\r
-});\r
-\r
-_kiwi.view.Channel = _kiwi.view.Panel.extend({\r
-    initialize: function (options) {\r
-        this.initializePanel(options);\r
-        this.model.bind('change:topic', this.topic, this);\r
-\r
-        // Only show the loader if this is a channel (ie. not a query)\r
-        if (this.model.isChannel()) {\r
-            this.$el.append('<div class="initial_loader" style="margin:1em;text-align:center;">Joining channel.. <span class="loader"></span></div>');\r
-        }\r
-    },\r
-\r
-    // Override the existing newMsg() method to remove the joining channel loader\r
-    newMsg: function () {\r
-        this.$el.find('.initial_loader').slideUp(function () {\r
-            $(this).remove();\r
-        });\r
-\r
-        return this.constructor.__super__.newMsg.apply(this, arguments);\r
-    },\r
-\r
-    topic: function (topic) {\r
-        if (typeof topic !== 'string' || !topic) {\r
-            topic = this.model.get("topic");\r
-        }\r
-\r
-        this.model.addMsg('', '== Topic for ' + this.model.get('name') + ' is: ' + topic, 'topic');\r
-\r
-        // If this is the active channel then update the topic bar\r
-        if (_kiwi.app.panels().active === this) {\r
-            _kiwi.app.topicbar.setCurrentTopic(this.model.get("topic"));\r
-        }\r
-    }\r
-});\r
-\r
-\r
-\r
-// Model for this = _kiwi.model.NetworkPanelList\r
-_kiwi.view.NetworkTabs = Backbone.View.extend({\r
-    tagName: 'ul',\r
-    className: 'connections',\r
-\r
-    initialize: function() {\r
-        this.model.on('add', this.networkAdded, this);\r
-        this.model.on('remove', this.networkRemoved, this);\r
-\r
-        this.$el.appendTo($('#kiwi .tabs'));\r
-    },\r
-\r
-    networkAdded: function(network) {\r
-        $('<li class="connection"></li>')\r
-            .append(network.panels.view.$el)\r
-            .appendTo(this.$el);\r
-    },\r
-\r
-    networkRemoved: function(network) {\r
-        network.panels.view.remove();\r
-\r
-        _kiwi.app.view.doLayout();\r
-    }\r
-});\r
-\r
-\r
-\r
-// Model for this = _kiwi.model.PanelList\r
-_kiwi.view.Tabs = Backbone.View.extend({\r
-    tagName: 'ul',\r
-    className: 'panellist',\r
-\r
-    events: {\r
-        'click li': 'tabClick',\r
-        'click li .part': 'partClick'\r
-    },\r
-\r
-    initialize: function () {\r
-        this.model.on("add", this.panelAdded, this);\r
-        this.model.on("remove", this.panelRemoved, this);\r
-        this.model.on("reset", this.render, this);\r
-\r
-        this.model.on('active', this.panelActive, this);\r
-\r
-        // Network tabs start with a server, so determine what we are now\r
-        this.is_network = false;\r
-\r
-        if (this.model.network) {\r
-            this.is_network = true;\r
-\r
-            this.model.network.on('change:name', function (network, new_val) {\r
-                $('span', this.model.server.tab).text(new_val);\r
-            }, this);\r
-        }\r
-    },\r
-\r
-    render: function () {\r
-        var that = this;\r
-\r
-        this.$el.empty();\r
-        \r
-        if (this.is_network) {\r
-            // Add the server tab first\r
-            this.model.server.tab\r
-                .data('panel', this.model.server)\r
-                .data('connection_id', this.model.network.get('connection_id'))\r
-                .appendTo(this.$el);\r
-        }\r
-\r
-        // Go through each panel adding its tab\r
-        this.model.forEach(function (panel) {\r
-            // If this is the server panel, ignore as it's already added\r
-            if (this.is_network && panel == that.model.server)\r
-                return;\r
-\r
-            panel.tab.data('panel', panel);\r
-\r
-            if (this.is_network)\r
-                panel.tab.data('connection_id', this.model.network.get('connection_id'));\r
-\r
-            panel.tab.appendTo(that.$el);\r
-        });\r
-\r
-        _kiwi.app.view.doLayout();\r
-    },\r
-\r
-    updateTabTitle: function (panel, new_title) {\r
-        $('span', panel.tab).text(new_title);\r
-    },\r
-\r
-    panelAdded: function (panel) {\r
-        // Add a tab to the panel\r
-        panel.tab = $('<li><span>' + (panel.get('title') || panel.get('name')) + '</span><div class="activity"></div></li>');\r
-\r
-        if (panel.isServer()) {\r
-            panel.tab.addClass('server');\r
-            panel.tab.addClass('icon-nonexistant');\r
-        }\r
-\r
-        panel.tab.data('panel', panel);\r
-\r
-        if (this.is_network)\r
-            panel.tab.data('connection_id', this.model.network.get('connection_id'));\r
-\r
-        panel.tab.appendTo(this.$el);\r
-\r
-        panel.bind('change:title', this.updateTabTitle);\r
-        panel.bind('change:name', this.updateTabTitle);\r
-\r
-        _kiwi.app.view.doLayout();\r
-    },\r
-    panelRemoved: function (panel) {\r
-        panel.tab.remove();\r
-        delete panel.tab;\r
-\r
-        _kiwi.app.view.doLayout();\r
-    },\r
-\r
-    panelActive: function (panel, previously_active_panel) {\r
-        // Remove any existing tabs or part images\r
-        _kiwi.app.view.$el.find('.panellist .part').remove();\r
-        _kiwi.app.view.$el.find('.panellist .active').removeClass('active');\r
-\r
-        panel.tab.addClass('active');\r
-\r
-        // Only show the part image on non-server tabs\r
-        if (!panel.isServer()) {\r
-            panel.tab.append('<span class="part icon-nonexistant"></span>');\r
-        }\r
-    },\r
-\r
-    tabClick: function (e) {\r
-        var tab = $(e.currentTarget);\r
-\r
-        var panel = tab.data('panel');\r
-        if (!panel) {\r
-            // A panel wasn't found for this tab... wadda fuck\r
-            return;\r
-        }\r
-\r
-        panel.view.show();\r
-    },\r
-\r
-    partClick: function (e) {\r
-        var tab = $(e.currentTarget).parent();\r
-        var panel = tab.data('panel');\r
-\r
-        if (!panel) return;\r
-\r
-        // Only need to part if it's a channel\r
-        // If the nicklist is empty, we haven't joined the channel as yet\r
-        if (panel.isChannel() && panel.get('members').models.length > 0) {\r
-            this.model.network.gateway.part(panel.get('name'));\r
-        } else {\r
-            panel.close();\r
-        }\r
-    }\r
-});\r
-\r
-\r
-\r
-_kiwi.view.TopicBar = Backbone.View.extend({\r
-    events: {\r
-        'keydown div': 'process'\r
-    },\r
-\r
-    initialize: function () {\r
-        _kiwi.app.panels.bind('active', function (active_panel) {\r
-            // If it's a channel topic, update and make editable\r
-            if (active_panel.isChannel()) {\r
-                this.setCurrentTopic(active_panel.get('topic') || '');\r
-                this.$el.find('div').attr('contentEditable', true);\r
-\r
-            } else {\r
-                // Not a channel topic.. clear and make uneditable\r
-                this.$el.find('div').attr('contentEditable', false)\r
-                    .text('');\r
-            }\r
-        }, this);\r
-    },\r
-\r
-    process: function (ev) {\r
-        var inp = $(ev.currentTarget),\r
-            inp_val = inp.text();\r
-        \r
-        // Only allow topic editing if this is a channel panel\r
-        if (!_kiwi.app.panels().active.isChannel()) {\r
-            return false;\r
-        }\r
-\r
-        // If hit return key, update the current topic\r
-        if (ev.keyCode === 13) {\r
-            _kiwi.gateway.topic(null, _kiwi.app.panels().active.get('name'), inp_val);\r
-            return false;\r
-        }\r
-    },\r
-\r
-    setCurrentTopic: function (new_topic) {\r
-        new_topic = new_topic || '';\r
-\r
-        // We only want a plain text version\r
-        $('div', this.$el).html(formatIRCMsg(_.escape(new_topic)));\r
-    }\r
-});\r
-\r
-\r
-\r
-_kiwi.view.ControlBox = Backbone.View.extend({\r
-    events: {\r
-        'keydown .inp': 'process',\r
-        'click .nick': 'showNickChange'\r
-    },\r
-\r
-    initialize: function () {\r
-        var that = this;\r
-\r
-        this.buffer = [];  // Stores previously run commands\r
-        this.buffer_pos = 0;  // The current position in the buffer\r
-\r
-        this.preprocessor = new InputPreProcessor();\r
-        this.preprocessor.recursive_depth = 5;\r
-\r
-        // Hold tab autocomplete data\r
-        this.tabcomplete = {active: false, data: [], prefix: ''};\r
-\r
-        // Keep the nick view updated with nick changes\r
-        _kiwi.app.connections.on('change:nick', function(connection) {\r
-            // Only update the nick view if it's the active connection\r
-            if (connection !== _kiwi.app.connections.active_connection)\r
-                return;\r
-\r
-            $('.nick', that.$el).text(connection.get('nick'));\r
-        });\r
-\r
-        // Update our nick view as we flick between connections\r
-        _kiwi.app.connections.on('active', function(panel, connection) {\r
-            $('.nick', that.$el).text(connection.get('nick'));\r
-        });\r
-    },\r
-\r
-    showNickChange: function (ev) {\r
-        (new _kiwi.view.NickChangeBox()).render();\r
-    },\r
-\r
-    process: function (ev) {\r
-        var that = this,\r
-            inp = $(ev.currentTarget),\r
-            inp_val = inp.val(),\r
-            meta;\r
-\r
-        if (navigator.appVersion.indexOf("Mac") !== -1) {\r
-            meta = ev.metaKey;\r
-        } else {\r
-            meta = ev.altKey;\r
-        }\r
-\r
-        // If not a tab key, reset the tabcomplete data\r
-        if (this.tabcomplete.active && ev.keyCode !== 9) {\r
-            this.tabcomplete.active = false;\r
-            this.tabcomplete.data = [];\r
-            this.tabcomplete.prefix = '';\r
-        }\r
-        \r
-        switch (true) {\r
-        case (ev.keyCode === 13):              // return\r
-            inp_val = inp_val.trim();\r
-\r
-            if (inp_val) {\r
-                $.each(inp_val.split('\n'), function (idx, line) {\r
-                    that.processInput(line);\r
-                });\r
-\r
-                this.buffer.push(inp_val);\r
-                this.buffer_pos = this.buffer.length;\r
-            }\r
-\r
-            inp.val('');\r
-            return false;\r
-\r
-            break;\r
-\r
-        case (ev.keyCode === 38):              // up\r
-            if (this.buffer_pos > 0) {\r
-                this.buffer_pos--;\r
-                inp.val(this.buffer[this.buffer_pos]);\r
-            }\r
-            //suppress browsers default behavior as it would set the cursor at the beginning\r
-            return false;\r
-\r
-        case (ev.keyCode === 40):              // down\r
-            if (this.buffer_pos < this.buffer.length) {\r
-                this.buffer_pos++;\r
-                inp.val(this.buffer[this.buffer_pos]);\r
-            }\r
-            break;\r
-\r
-        case (ev.keyCode === 219 && meta):            // [ + meta\r
-            // Find all the tab elements and get the index of the active tab\r
-            var $tabs = $('#kiwi .tabs').find('li[class!=connection]');\r
-            var cur_tab_ind = (function() {\r
-                for (var idx=0; idx<$tabs.length; idx++){\r
-                    if ($($tabs[idx]).hasClass('active'))\r
-                        return idx;\r
-                }\r
-            })();\r
-\r
-            // Work out the previous tab along. Wrap around if needed\r
-            if (cur_tab_ind === 0) {\r
-                $prev_tab = $($tabs[$tabs.length - 1]);\r
-            } else {\r
-                $prev_tab = $($tabs[cur_tab_ind - 1]);\r
-            }\r
-\r
-            $prev_tab.click();\r
-            return false;\r
-\r
-        case (ev.keyCode === 221 && meta):            // ] + meta\r
-            // Find all the tab elements and get the index of the active tab\r
-            var $tabs = $('#kiwi .tabs').find('li[class!=connection]');\r
-            var cur_tab_ind = (function() {\r
-                for (var idx=0; idx<$tabs.length; idx++){\r
-                    if ($($tabs[idx]).hasClass('active'))\r
-                        return idx;\r
-                }\r
-            })();\r
-\r
-            // Work out the next tab along. Wrap around if needed\r
-            if (cur_tab_ind === $tabs.length - 1) {\r
-                $next_tab = $($tabs[0]);\r
-            } else {\r
-                $next_tab = $($tabs[cur_tab_ind + 1]);\r
-            }\r
-\r
-            $next_tab.click();\r
-            return false;\r
-\r
-        case (ev.keyCode === 9):                     // tab\r
-            this.tabcomplete.active = true;\r
-            if (_.isEqual(this.tabcomplete.data, [])) {\r
-                // Get possible autocompletions\r
-                var ac_data = [],\r
-                    members = _kiwi.app.panels().active.get('members');\r
-\r
-                // If we have a members list, get the models. Otherwise empty array\r
-                members = members ? members.models : [];\r
-\r
-                $.each(members, function (i, member) {\r
-                    if (!member) return;\r
-                    ac_data.push(member.get('nick'));\r
-                });\r
-\r
-                ac_data.push(_kiwi.app.panels().active.get('name'));\r
-\r
-                ac_data = _.sortBy(ac_data, function (nick) {\r
-                    return nick;\r
-                });\r
-                this.tabcomplete.data = ac_data;\r
-            }\r
-\r
-            if (inp_val[inp[0].selectionStart - 1] === ' ') {\r
-                return false;\r
-            }\r
-            \r
-            (function () {\r
-                var tokens,              // Words before the cursor position\r
-                    val,                 // New value being built up\r
-                    p1,                  // Position in the value just before the nick \r
-                    newnick,             // New nick to be displayed (cycles through)\r
-                    range,               // TextRange for setting new text cursor position\r
-                    nick,                // Current nick in the value\r
-                    trailing = ': ';     // Text to be inserted after a tabbed nick\r
-\r
-                tokens = inp_val.substring(0, inp[0].selectionStart).split(' ');\r
-                if (tokens[tokens.length-1] == ':')\r
-                    tokens.pop();\r
-\r
-                nick  = tokens[tokens.length - 1];\r
-\r
-                if (this.tabcomplete.prefix === '') {\r
-                    this.tabcomplete.prefix = nick;\r
-                }\r
-\r
-                this.tabcomplete.data = _.select(this.tabcomplete.data, function (n) {\r
-                    return (n.toLowerCase().indexOf(that.tabcomplete.prefix.toLowerCase()) === 0);\r
-                });\r
-\r
-                if (this.tabcomplete.data.length > 0) {\r
-                    // Get the current value before cursor position\r
-                    p1 = inp[0].selectionStart - (nick.length);\r
-                    val = inp_val.substr(0, p1);\r
-\r
-                    // Include the current selected nick\r
-                    newnick = this.tabcomplete.data.shift();\r
-                    this.tabcomplete.data.push(newnick);\r
-                    val += newnick;\r
-\r
-                    if (inp_val.substr(inp[0].selectionStart, 2) !== trailing)\r
-                        val += trailing;\r
-\r
-                    // Now include the rest of the current value\r
-                    val += inp_val.substr(inp[0].selectionStart);\r
-\r
-                    inp.val(val);\r
-\r
-                    // Move the cursor position to the end of the nick\r
-                    if (inp[0].setSelectionRange) {\r
-                        inp[0].setSelectionRange(p1 + newnick.length + trailing.length, p1 + newnick.length + trailing.length);\r
-                    } else if (inp[0].createTextRange) { // not sure if this bit is actually needed....\r
-                        range = inp[0].createTextRange();\r
-                        range.collapse(true);\r
-                        range.moveEnd('character', p1 + newnick.length + trailing.length);\r
-                        range.moveStart('character', p1 + newnick.length + trailing.length);\r
-                        range.select();\r
-                    }\r
-                }\r
-            }).apply(this);\r
-            return false;\r
-        }\r
-    },\r
-\r
-\r
-    processInput: function (command_raw) {\r
-        var command, params,\r
-            pre_processed;\r
-        \r
-        // The default command\r
-        if (command_raw[0] !== '/' || command_raw.substr(0, 2) === '//') {\r
-            // Remove any slash escaping at the start (ie. //)\r
-            command_raw = command_raw.replace(/^\/\//, '/');\r
-\r
-            // Prepend the default command\r
-            command_raw = '/msg ' + _kiwi.app.panels().active.get('name') + ' ' + command_raw;\r
-        }\r
-\r
-        // Process the raw command for any aliases\r
-        this.preprocessor.vars.server = _kiwi.app.connections.active_connection.get('name');\r
-        this.preprocessor.vars.channel = _kiwi.app.panels().active.get('name');\r
-        this.preprocessor.vars.destination = this.preprocessor.vars.channel;\r
-        command_raw = this.preprocessor.process(command_raw);\r
-\r
-        // Extract the command and parameters\r
-        params = command_raw.split(' ');\r
-        if (params[0][0] === '/') {\r
-            command = params[0].substr(1).toLowerCase();\r
-            params = params.splice(1, params.length - 1);\r
-        } else {\r
-            // Default command\r
-            command = 'msg';\r
-            params.unshift(_kiwi.app.panels().active.get('name'));\r
-        }\r
-\r
-        // Trigger the command events\r
-        this.trigger('command', {command: command, params: params});\r
-        this.trigger('command:' + command, {command: command, params: params});\r
-\r
-        // If we didn't have any listeners for this event, fire a special case\r
-        // TODO: This feels dirty. Should this really be done..?\r
-        if (!this._events['command:' + command]) {\r
-            this.trigger('unknown_command', {command: command, params: params});\r
-        }\r
-    },\r
-\r
-\r
-    addPluginIcon: function ($icon) {\r
-        var $tool = $('<div class="tool"></div>').append($icon);\r
-        this.$el.find('.input_tools').append($tool);\r
-        _kiwi.app.view.doLayout();\r
-    }\r
-});\r
-\r
-\r
-\r
-\r
-_kiwi.view.StatusMessage = Backbone.View.extend({\r
-    initialize: function () {\r
-        this.$el.hide();\r
-\r
-        // Timer for hiding the message after X seconds\r
-        this.tmr = null;\r
-    },\r
-\r
-    text: function (text, opt) {\r
-        // Defaults\r
-        opt = opt || {};\r
-        opt.type = opt.type || '';\r
-        opt.timeout = opt.timeout || 5000;\r
-\r
-        this.$el.text(text).addClass(opt.type);\r
-        this.$el.slideDown($.proxy(_kiwi.app.view.doLayout, _kiwi.app.view));\r
-\r
-        if (opt.timeout) this.doTimeout(opt.timeout);\r
-    },\r
-\r
-    html: function (html, opt) {\r
-        // Defaults\r
-        opt = opt || {};\r
-        opt.type = opt.type || '';\r
-        opt.timeout = opt.timeout || 5000;\r
-\r
-        this.$el.html(text).addClass(opt.type);\r
-        this.$el.slideDown($.proxy(_kiwi.app.view.doLayout, _kiwi.app.view));\r
-\r
-        if (opt.timeout) this.doTimeout(opt.timeout);\r
-    },\r
-\r
-    hide: function () {\r
-        this.$el.slideUp($.proxy(_kiwi.app.view.doLayout, _kiwi.app.view));\r
-    },\r
-\r
-    doTimeout: function (length) {\r
-        if (this.tmr) clearTimeout(this.tmr);\r
-        var that = this;\r
-        this.tmr = setTimeout(function () { that.hide(); }, length);\r
-    }\r
-});\r
-\r
-\r
-\r
-\r
-_kiwi.view.ResizeHandler = Backbone.View.extend({\r
-    events: {\r
-        'mousedown': 'startDrag',\r
-        'mouseup': 'stopDrag'\r
-    },\r
-\r
-    initialize: function () {\r
-        this.dragging = false;\r
-        this.starting_width = {};\r
-\r
-        $(window).on('mousemove', $.proxy(this.onDrag, this));\r
-    },\r
-\r
-    startDrag: function (event) {\r
-        this.dragging = true;\r
-    },\r
-\r
-    stopDrag: function (event) {\r
-        this.dragging = false;\r
-    },\r
-\r
-    onDrag: function (event) {\r
-        if (!this.dragging) return;\r
-\r
-        this.$el.css('left', event.clientX - (this.$el.outerWidth(true) / 2));\r
-        $('#kiwi .memberlists').css('width', this.$el.parent().width() - (this.$el.position().left + this.$el.outerWidth()));\r
-        _kiwi.app.view.doLayout();\r
-    }\r
-});\r
-\r
-\r
-\r
-_kiwi.view.AppToolbar = Backbone.View.extend({\r
-    events: {\r
-        'click .settings': 'clickSettings'\r
-    },\r
-\r
-    initialize: function () {\r
-    },\r
-\r
-    clickSettings: function (event) {\r
-        _kiwi.app.controlbox.processInput('/settings');\r
-    }\r
-});\r
-\r
-\r
-\r
-_kiwi.view.Application = Backbone.View.extend({\r
-    initialize: function () {\r
-        var that = this;\r
-\r
-        $(window).resize(function() { that.doLayout.apply(that); });\r
-        this.$el.find('.toolbar').resize(function() { that.doLayout.apply(that); });\r
-        $('#kiwi .controlbox').resize(function() { that.doLayout.apply(that); });\r
-\r
-        // Change the theme when the config is changed\r
-        _kiwi.global.settings.on('change:theme', this.updateTheme, this);\r
-        this.updateTheme(getQueryVariable('theme'));\r
-\r
-        _kiwi.global.settings.on('change:channel_list_style', this.setTabLayout, this);\r
-        this.setTabLayout(_kiwi.global.settings.get('channel_list_style'));\r
-\r
-        _kiwi.global.settings.on('change:show_timestamps', this.displayTimestamps, this);\r
-        this.displayTimestamps(_kiwi.global.settings.get('show_timestamps'));\r
-\r
-        this.doLayout();\r
-\r
-        $(document).keydown(this.setKeyFocus);\r
-\r
-        // Confirmation require to leave the page\r
-        window.onbeforeunload = function () {\r
-            if (_kiwi.gateway.isConnected()) {\r
-                return 'This will close all KiwiIRC conversations. Are you sure you want to close this window?';\r
-            }\r
-        };\r
-\r
-        this.initSound();\r
-    },\r
-\r
-\r
-\r
-    updateTheme: function (theme_name) {\r
-        // If called by the settings callback, get the correct new_value\r
-        if (theme_name === _kiwi.global.settings) {\r
-            theme_name = arguments[1];\r
-        }\r
-\r
-        // If we have no theme specified, get it from the settings\r
-        if (!theme_name) theme_name = _kiwi.global.settings.get('theme');\r
-\r
-        // Clear any current theme\r
-        this.$el.removeClass(function (i, css) {\r
-            return (css.match(/\btheme_\S+/g) || []).join(' ');\r
-        });\r
-\r
-        // Apply the new theme\r
-        this.$el.addClass('theme_' + (theme_name || 'relaxed'));\r
-    },\r
-\r
-\r
-    setTabLayout: function (layout_style) {\r
-        // If called by the settings callback, get the correct new_value\r
-        if (layout_style === _kiwi.global.settings) {\r
-            layout_style = arguments[1];\r
-        }\r
-        \r
-        if (layout_style == 'list') {\r
-            this.$el.addClass('chanlist_treeview');\r
-        } else {\r
-            this.$el.removeClass('chanlist_treeview');\r
-        }\r
-        \r
-        this.doLayout();\r
-    },\r
-\r
-\r
-    displayTimestamps: function (show_timestamps) {\r
-        // If called by the settings callback, get the correct new_value\r
-        if (show_timestamps === _kiwi.global.settings) {\r
-            show_timestamps = arguments[1];\r
-        }\r
-\r
-        if (show_timestamps) {\r
-            this.$el.addClass('timestamps');\r
-        } else {\r
-            this.$el.removeClass('timestamps');\r
-        }\r
-    },\r
-\r
-\r
-    // Globally shift focus to the command input box on a keypress\r
-    setKeyFocus: function (ev) {\r
-        // If we're copying text, don't shift focus\r
-        if (ev.ctrlKey || ev.altKey || ev.metaKey) {\r
-            return;\r
-        }\r
-\r
-        // If we're typing into an input box somewhere, ignore\r
-        if ((ev.target.tagName.toLowerCase() === 'input') || (ev.target.tagName.toLowerCase() === 'textarea') || $(ev.target).attr('contenteditable')) {\r
-            return;\r
-        }\r
-\r
-        $('#kiwi .controlbox .inp').focus();\r
-    },\r
-\r
-\r
-    doLayout: function () {\r
-        var el_kiwi = this.$el;\r
-        var el_panels = $('#kiwi .panels');\r
-        var el_memberlists = $('#kiwi .memberlists');\r
-        var el_toolbar = this.$el.find('.toolbar');\r
-        var el_controlbox = $('#kiwi .controlbox');\r
-        var el_resize_handle = $('#kiwi .memberlists_resize_handle');\r
-\r
-        var css_heights = {\r
-            top: el_toolbar.outerHeight(true),\r
-            bottom: el_controlbox.outerHeight(true)\r
-        };\r
-\r
-\r
-        // If any elements are not visible, full size the panals instead\r
-        if (!el_toolbar.is(':visible')) {\r
-            css_heights.top = 0;\r
-        }\r
-\r
-        if (!el_controlbox.is(':visible')) {\r
-            css_heights.bottom = 0;\r
-        }\r
-\r
-        // Apply the CSS sizes\r
-        el_panels.css(css_heights);\r
-        el_memberlists.css(css_heights);\r
-        el_resize_handle.css(css_heights);\r
-\r
-        // If we have channel tabs on the side, adjust the height\r
-        if (el_kiwi.hasClass('chanlist_treeview')) {\r
-            this.$el.find('.tabs', el_kiwi).css(css_heights);\r
-        }\r
-\r
-        // Determine if we have a narrow window (mobile/tablet/or even small desktop window)\r
-        if (el_kiwi.outerWidth() < 400) {\r
-            el_kiwi.addClass('narrow');\r
-        } else {\r
-            el_kiwi.removeClass('narrow');\r
-        }\r
-\r
-        // Set the panels width depending on the memberlist visibility\r
-        if (el_memberlists.css('display') != 'none') {\r
-            // Panels to the side of the memberlist\r
-            el_panels.css('right', el_memberlists.outerWidth(true));\r
-            // The resize handle sits overlapping the panels and memberlist\r
-            el_resize_handle.css('left', el_memberlists.position().left - (el_resize_handle.outerWidth(true) / 2));\r
-        } else {\r
-            // Memberlist is hidden so panels to the right edge\r
-            el_panels.css('right', 0);\r
-            // And move the handle just out of sight to the right\r
-            el_resize_handle.css('left', el_panels.outerWidth(true));\r
-        }\r
-\r
-        var input_wrap_width = parseInt($('#kiwi .controlbox .input_tools').outerWidth());\r
-        el_controlbox.find('.input_wrap').css('right', input_wrap_width + 7);\r
-    },\r
-\r
-\r
-    alertWindow: function (title) {\r
-        if (!this.alertWindowTimer) {\r
-            this.alertWindowTimer = new (function () {\r
-                var that = this;\r
-                var tmr;\r
-                var has_focus = true;\r
-                var state = 0;\r
-                var default_title = 'Kiwi IRC';\r
-                var title = 'Kiwi IRC';\r
-\r
-                this.setTitle = function (new_title) {\r
-                    new_title = new_title || default_title;\r
-                    window.document.title = new_title;\r
-                    return new_title;\r
-                };\r
-\r
-                this.start = function (new_title) {\r
-                    // Don't alert if we already have focus\r
-                    if (has_focus) return;\r
-\r
-                    title = new_title;\r
-                    if (tmr) return;\r
-                    tmr = setInterval(this.update, 1000);\r
-                };\r
-\r
-                this.stop = function () {\r
-                    // Stop the timer and clear the title\r
-                    if (tmr) clearInterval(tmr);\r
-                    tmr = null;\r
-                    this.setTitle();\r
-\r
-                    // Some browsers don't always update the last title correctly\r
-                    // Wait a few seconds and then reset\r
-                    setTimeout(this.reset, 2000);\r
-                };\r
-\r
-                this.reset = function () {\r
-                    if (tmr) return;\r
-                    that.setTitle();\r
-                };\r
-\r
-\r
-                this.update = function () {\r
-                    if (state === 0) {\r
-                        that.setTitle(title);\r
-                        state = 1;\r
-                    } else {\r
-                        that.setTitle();\r
-                        state = 0;\r
-                    }\r
-                };\r
-\r
-                $(window).focus(function (event) {\r
-                    has_focus = true;\r
-                    that.stop();\r
-\r
-                    // Some browsers don't always update the last title correctly\r
-                    // Wait a few seconds and then reset\r
-                    setTimeout(that.reset, 2000);\r
-                });\r
-\r
-                $(window).blur(function (event) {\r
-                    has_focus = false;\r
-                });\r
-            })();\r
-        }\r
-\r
-        this.alertWindowTimer.start(title);\r
-    },\r
-\r
-\r
-    barsHide: function (instant) {\r
-        var that = this;\r
-\r
-        if (!instant) {\r
-            this.$el.find('.toolbar').slideUp({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});\r
-            $('#kiwi .controlbox').slideUp({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});\r
-        } else {\r
-            this.$el.find('.toolbar').slideUp(0);\r
-            $('#kiwi .controlbox').slideUp(0);\r
-            this.doLayout();\r
-        }\r
-    },\r
-\r
-    barsShow: function (instant) {\r
-        var that = this;\r
-\r
-        if (!instant) {\r
-            this.$el.find('.toolbar').slideDown({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});\r
-            $('#kiwi .controlbox').slideDown({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});\r
-        } else {\r
-            this.$el.find('.toolbar').slideDown(0);\r
-            $('#kiwi .controlbox').slideDown(0);\r
-            this.doLayout();\r
-        }\r
-    },\r
-\r
-\r
-    initSound: function () {\r
-        var that = this,\r
-            base_path = this.model.get('base_path');\r
-\r
-        $script(base_path + '/assets/libs/soundmanager2/soundmanager2-nodebug-jsmin.js', function() {\r
-            if (typeof soundManager === 'undefined')\r
-                return;\r
-\r
-            soundManager.setup({\r
-                url: base_path + '/assets/libs/soundmanager2/',\r
-                flashVersion: 9, // optional: shiny features (default = 8)// optional: ignore Flash where possible, use 100% HTML5 mode\r
-                preferFlash: true,\r
-\r
-                onready: function() {\r
-                    that.sound_object = soundManager.createSound({\r
-                        id: 'highlight',\r
-                        url: base_path + '/assets/sound/highlight.mp3'\r
-                    });\r
-                }\r
-            });\r
-        });\r
-    },\r
-\r
-\r
-    playSound: function (sound_id) {\r
-        if (!this.sound_object) return;\r
-\r
-        if (_kiwi.global.settings.get('mute_sounds'))\r
-            return;\r
-        \r
-        soundManager.play(sound_id);\r
-    }\r
-});\r
-\r
-\r
-\r
-\r
-\r
-\r
-\r
-\r
-\r
-_kiwi.view.MediaMessage = Backbone.View.extend({\r
-    events: {\r
-        'click .media_close': 'close'\r
-    },\r
-\r
-    initialize: function () {\r
-        // Get the URL from the data\r
-        this.url = this.$el.data('url');\r
-    },\r
-\r
-    // Close the media content and remove it from display\r
-    close: function () {\r
-        var that = this;\r
-        this.$content.slideUp('fast', function () {\r
-            that.$content.remove();\r
-        });\r
-    },\r
-\r
-    // Open the media content within its wrapper\r
-    open: function () {\r
-        // Create the content div if we haven't already\r
-        if (!this.$content) {\r
-            this.$content = $('<div class="media_content"><a class="media_close"><i class="icon-chevron-up"></i> Close media</a><br /><div class="content"></div></div>');\r
-            this.$content.find('.content').append(this.mediaTypes[this.$el.data('type')].apply(this, []) || 'Not found :(');\r
-        }\r
-\r
-        // Now show the content if not already\r
-        if (!this.$content.is(':visible')) {\r
-            // Hide it first so the slideDown always plays\r
-            this.$content.hide();\r
-\r
-            // Add the media content and slide it into view\r
-            this.$el.append(this.$content);\r
-            this.$content.slideDown();\r
-        }\r
-    },\r
-\r
-\r
-\r
-    // Generate the media content for each recognised type\r
-    mediaTypes: {\r
-        twitter: function () {\r
-            var tweet_id = this.$el.data('tweetid');\r
-            var that = this;\r
-\r
-            $.getJSON('https://api.twitter.com/1/statuses/oembed.json?id=' + tweet_id + '&callback=?', function (data) {\r
-                that.$content.find('.content').html(data.html);\r
-            });\r
-\r
-            return $('<div>Loading tweet..</div>');\r
-        },\r
-\r
-\r
-        image: function () {\r
-            return $('<a href="' + this.url + '" target="_blank"><img height="100" src="' + this.url + '" /></a>');\r
-        },\r
-\r
-\r
-        reddit: function () {\r
-            var that = this;\r
-            var matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(this.url);\r
-\r
-            $.getJSON('http://www.' + matches[0] + '.json?jsonp=?', function (data) {\r
-                console.log('Loaded reddit data', data);\r
-                var post = data[0].data.children[0].data;\r
-                var thumb = '';\r
-\r
-                // Show a thumbnail if there is one\r
-                if (post.thumbnail) {\r
-                    //post.thumbnail = 'http://www.eurotunnel.com/uploadedImages/commercial/back-steps-icon-arrow.png';\r
-\r
-                    // Hide the thumbnail if an over_18 image\r
-                    if (post.over_18) {\r
-                        thumb = '<span class="thumbnail_nsfw" onclick="$(this).find(\'p\').remove(); $(this).find(\'img\').css(\'visibility\', \'visible\');">';\r
-                        thumb += '<p style="font-size:0.9em;line-height:1.2em;cursor:pointer;">Show<br />NSFW</p>';\r
-                        thumb += '<img src="' + post.thumbnail + '" class="thumbnail" style="visibility:hidden;" />';\r
-                        thumb += '</span>';\r
-                    } else {\r
-                        thumb = '<img src="' + post.thumbnail + '" class="thumbnail" />';\r
-                    }\r
-                }\r
-\r
-                // Build the template string up\r
-                var tmpl = '<div>' + thumb + '<b><%- title %></b><br />Posted by <%- author %>. &nbsp;&nbsp; ';\r
-                tmpl += '<i class="icon-arrow-up"></i> <%- ups %> &nbsp;&nbsp; <i class="icon-arrow-down"></i> <%- downs %><br />';\r
-                tmpl += '<%- num_comments %> comments made. <a href="http://www.reddit.com<%- permalink %>">View post</a></div>';\r
-\r
-                that.$content.find('.content').html(_.template(tmpl, post));\r
-            });\r
-\r
-            return $('<div>Loading Reddit thread..</div>');\r
-        }\r
-    }\r
-\r
-}, {\r
-\r
-    // Build the closed media HTML from a URL\r
-    buildHtml: function (url) {\r
-        var html = '', matches;\r
-\r
-        // Is it an image?\r
-        if (url.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)) {\r
-            html += '<span class="media image" data-type="image" data-url="' + url + '" title="Open Image"><a class="open"><i class="icon-chevron-right"></i></a></span>';\r
-        }\r
-\r
-        // Is it a tweet?\r
-        matches = (/https?:\/\/twitter.com\/([a-zA-Z0-9_]+)\/status\/([0-9]+)/ig).exec(url);\r
-        if (matches) {\r
-            html += '<span class="media twitter" data-type="twitter" data-url="' + url + '" data-tweetid="' + matches[2] + '" title="Show tweet information"><a class="open"><i class="icon-chevron-right"></i></a></span>';\r
-        }\r
-\r
-        // Is reddit?\r
-        matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(url);\r
-        if (matches) {\r
-            html += '<span class="media reddit" data-type="reddit" data-url="' + url + '" title="Reddit thread"><a class="open"><i class="icon-chevron-right"></i></a></span>';\r
-        }\r
-\r
-        return html;\r
-    }\r
-});\r
-\r
-\r
-\r
-_kiwi.view.MenuBox = Backbone.View.extend({\r
-    events: {\r
-        'click .ui_menu_foot .close': 'dispose'\r
-    },\r
-\r
-    initialize: function(title) {\r
-        var that = this;\r
-\r
-        this.$el = $('<div class="ui_menu"></div>');\r
-\r
-        this._title = title || '';\r
-        this._items = {};\r
-        this._display_footer = true;\r
-        this._close_on_blur = true;\r
-\r
-        this._close_proxy = function(event) {\r
-            that.onDocumentClick(event);\r
-        };\r
-        $(document).on('click', this._close_proxy);\r
-    },\r
-\r
-\r
-    render: function() {\r
-        var that = this;\r
-\r
-        this.$el.find('*').remove();\r
-\r
-        if (this._title) {\r
-            $('<div class="ui_menu_title"></div>')\r
-                .text(this._title)\r
-                .appendTo(this.$el);\r
-        }\r
-\r
-\r
-        _.each(this._items, function(item) {\r
-            var $item = $('<div class="ui_menu_content hover"></div>')\r
-                .append(item);\r
-\r
-            that.$el.append($item);\r
-        });\r
-\r
-        if (this._display_footer)\r
-            this.$el.append('<div class="ui_menu_foot"><a class="close" onclick="">Close <i class="icon-remove"></i></a></div>');\r
-    },\r
-\r
-\r
-    onDocumentClick: function(event) {\r
-        var $target = $(event.target);\r
-\r
-        if (!this._close_on_blur)\r
-            return;\r
-\r
-        // If this is not itself AND we don't contain this element, dispose $el\r
-        if ($target[0] != this.$el[0] && this.$el.has($target).length === 0)\r
-            this.dispose();\r
-    },\r
-\r
-\r
-    dispose: function() {\r
-        _.each(this._items, function(item) {\r
-            item.dispose && item.dispose();\r
-            item.remove && item.remove();\r
-        });\r
-\r
-        this._items = null;\r
-        this.remove();\r
-\r
-        $(document).off('click', this._close_proxy);\r
-    },\r
-\r
-\r
-    addItem: function(item_name, $item) {\r
-        $item = $($item);\r
-        if ($item.is('a')) $item.addClass('icon-chevron-right');\r
-        this._items[item_name] = $item;\r
-    },\r
-\r
-\r
-    removeItem: function(item_name) {\r
-        delete this._items[item_name];\r
-    },\r
-\r
-\r
-    showFooter: function(show) {\r
-        this._display_footer = show;\r
-    },\r
-\r
-\r
-    closeOnBlur: function(close_it) {\r
-        this._close_on_blur = close_it;\r
-    },\r
-\r
-\r
-    show: function() {\r
-        this.render();\r
-        this.$el.appendTo(_kiwi.app.view.$el);\r
-    }\r
-});\r