Removing raw access to tabview list, now uses static methods on Tabview
[KiwiIRC.git] / js / front.js
index e0c107f95c25dc31eaecd61bafd98cf72c138e87..5276cea41d22dacfa3073f597f1c2cbc47d4485e 100644 (file)
@@ -1,27 +1,25 @@
-/*jslint nomen: true, devel: true, undef: true, browser: true, continue: true, sloppy: true, forin: true, newcap: true, plusplus: true, maxerr: 50, indent: 4 */
-/*global gateway, io, $, iScroll, agent, touchscreen, init_data, plugs, plugins, registerTouches, randomString */
+/*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 */
+/*global kiwi, _, io, $, iScroll, agent, touchscreen, init_data, plugs, plugins, registerTouches, randomString */
 kiwi.front = {
-    revision: 38,
-    
     cur_channel: '',
     windows: {},
     tabviews: {},
     utilityviews: {},
     boxes: {},
-    
+
     buffer: [],
     buffer_pos: 0,
 
     cache: {},
-    
+
     original_topic: '',
-    
+
     init: function () {
-        /*global Box, touch_scroll:true */
-        var about_info, supportsOrientationChange, orientationEvent, scroll_opts;
+        /*global Box, touch_scroll:true, Tabview */
+        var about_info, supportsOrientationChange, orientationEvent, scroll_opts, server_tabview;
         kiwi.gateway.nick = 'kiwi_' + Math.ceil(100 * Math.random()) + Math.ceil(100 * Math.random());
         kiwi.gateway.session_id = null;
-        
+
         $(kiwi.gateway).bind("onmsg", kiwi.front.onMsg);
         $(kiwi.gateway).bind("onnotice", kiwi.front.onNotice);
         $(kiwi.gateway).bind("onaction", kiwi.front.onAction);
@@ -30,6 +28,7 @@ kiwi.front = {
         $(kiwi.gateway).bind("onconnect", kiwi.front.onConnect);
         $(kiwi.gateway).bind("onconnect_fail", kiwi.front.onConnectFail);
         $(kiwi.gateway).bind("ondisconnect", kiwi.front.onDisconnect);
+        $(kiwi.gateway).bind("onreconnecting", kiwi.front.onReconnecting);
         $(kiwi.gateway).bind("onnick", kiwi.front.onNick);
         $(kiwi.gateway).bind("onuserlist", kiwi.front.onUserList);
         $(kiwi.gateway).bind("onuserlist_end", kiwi.front.onUserListEnd);
@@ -50,9 +49,9 @@ kiwi.front = {
         $(kiwi.gateway).bind("onctcp_response", kiwi.front.onCTCPResponse);
         $(kiwi.gateway).bind("onirc_error", kiwi.front.onIRCError);
         $(kiwi.gateway).bind("onkiwi", kiwi.front.onKiwi);
-        
+
         this.buffer = [];
-        
+
         // Build the about box
         kiwi.front.boxes.about = new Box("about");
         about_info = 'UI adapted for ' + agent;
@@ -61,13 +60,11 @@ kiwi.front = {
         }
         about_info += 'usage';
         $('#tmpl_about_box').tmpl({
-            about: about_info,
-            front_revision: kiwi.front.revision,
-            gateway_revision: kiwi.gateway.revision
+            about: about_info
         }).appendTo(kiwi.front.boxes.about.content);
 
         //$(window).bind("beforeunload", function(){ kiwi.gateway.quit(); });
-        
+
         if (touchscreen) {
             $('#kiwi').addClass('touchscreen');
 
@@ -77,7 +74,7 @@ kiwi.front = {
         }
 
         kiwi.front.registerKeys();
-        
+
         $('#kiwi .toolbars').resize(kiwi.front.doLayoutSize);
         $(window).resize(kiwi.front.doLayoutSize);
 
@@ -85,33 +82,36 @@ kiwi.front = {
         $('<div id="nicklist_resize" style="position:absolute; cursor:w-resize; width:5px;"></div>').appendTo('#kiwi');
         $('#nicklist_resize').draggable({axis: "x", drag: function () {
             var t = $(this),
+                ul = $('#kiwi .userlist'),
                 new_width = $(document).width() - parseInt(t.css('left'), 10);
-            
-            new_width = new_width - parseInt($('#kiwi .userlist').css('margin-left'), 10);
-            new_width = new_width - parseInt($('#kiwi .userlist').css('margin-right'), 10);
+
+            new_width = new_width - parseInt(ul.css('margin-left'), 10);
+            new_width = new_width - parseInt(ul.css('margin-right'), 10);
 
             // Make sure we don't remove the userlist alltogether
-            console.log(new_width);
             if (new_width < 20) {
                 $(this).data('draggable').offset.click.left = 10;
                 console.log('whoaa');
             }
 
-            kiwi.front.cur_channel.setUserlistWidth(new_width);
+            Tabview.getCurrentTab().userlist.setWidth(new_width);
+            $('#windows').css('right', ul.outerWidth(true));
         }});
 
 
         $('#kiwi .formconnectwindow').submit(function () {
             var netsel = $('#kiwi .formconnectwindow .network'),
+                netport = $('#kiwi .formconnectwindow .port'),
+                netssl = $('#kiwi .formconnectwindow .ssl'),
                 nick = $('#kiwi .formconnectwindow .nick'),
                 tmp;
-            
+
             if (nick.val() === '') {
                 nick.val('Nick please!');
                 nick.focus();
                 return false;
             }
-            
+
             tmp = nick.val().split(' ');
             kiwi.gateway.nick = tmp[0];
 
@@ -119,9 +119,11 @@ kiwi.front = {
 
             kiwi.front.doLayout();
             try {
-                kiwi.front.run('/connect ' + netsel.val());
-            } catch (e) {}
-            
+                kiwi.front.run('/connect ' + netsel.val() + ' ' + netport.val() + ' ' + (netssl.attr('checked') ? 'true' : ''));
+            } catch (e) {
+                console.log(e);
+            }
+
             $('#kiwi .connectwindow').slideUp('', kiwi.front.barsShow);
             $('#windows').click(function () { $('#kiwi_msginput').focus(); });
 
@@ -140,10 +142,10 @@ kiwi.front = {
 
         kiwi.front.doLayout();
         kiwi.front.barsHide();
-        
-        kiwi.front.tabviewAdd('server');
-        kiwi.front.tabviews.server.userlist_width = 0; // Disable the userlist
-        
+
+        server_tabview = new Tabview('server');
+        server_tabview.userlist.setWidth(0); // Disable the userlist
+
         // Any pre-defined nick?
         if (typeof window.init_data.nick === "string") {
             $('#kiwi .formconnectwindow .nick').val(init_data.nick);
@@ -153,10 +155,10 @@ kiwi.front = {
         if (typeof window.init_data.channel === 'string') {
             $('#channel').val(init_data.channel);
         }
-        
+
         // Fix for Opera inserting a spurious <br/>
         $('#kiwi .cur_topic br').remove();
-        
+
         $('#kiwi .cur_topic').keydown(function (e) {
             if (e.which === 13) {
                 // enter
@@ -186,7 +188,7 @@ kiwi.front = {
             var chan, text;
             text = $(this).text();
             if (text !== kiwi.front.original_topic) {
-                chan = kiwi.front.cur_channel.name;
+                chan = Tabview.getCurrentTab().name;
                 kiwi.gateway.setTopic(chan, text);
             }
         });
@@ -196,7 +198,7 @@ kiwi.front = {
             kiwi.front.joinChannel($(this).text());
             return false;
         });
-        
+
         kiwi.data.set('chanList', []);
 
         (function () {
@@ -206,7 +208,7 @@ kiwi.front = {
             }
         }());
     },
-    
+
     doLayoutSize: function () {
         var kiwi, toolbars, ul, n_top, n_bottom, nl;
         kiwi = $('#kiwi');
@@ -239,27 +241,29 @@ kiwi.front = {
         $('#kiwi_msginput').val(' ');
         $('#kiwi_msginput').focus();
     },
-    
-    
+
+
     joinChannel: function (chan_name) {
         var chans = chan_name.split(','),
             i,
-            chan;
+            chan,
+            tab;
         for (i in chans) {
             chan = chans[i];
-            if (kiwi.front.tabviews[chan.toLowerCase()] === undefined || (kiwi.front.tabviews[chan.toLowerCase()] !== undefined && kiwi.front.tabviews[chan.toLowerCase()].safe_to_close === true)) {
+            tab = Tabview.getTab(chan);
+            if ((!tab) || (tab.safe_to_close === true)) {
                 kiwi.gateway.join(chan);
-                kiwi.front.tabviewAdd(chan);
+                tab = new Tabview(chan);
             } else {
-                kiwi.front.tabviews[chan.toLowerCase()].show();
+                tab.show();
             }
         }
     },
-    
-    
+
+
     run: function (msg) {
-        var parts, dest, t, pos, textRange, d, plugin_event, msg_sliced;
-        
+        var parts, dest, t, pos, textRange, d, plugin_event, msg_sliced, tab;
+
         // Run through any plugins
         plugin_event = {command: msg};
         plugin_event = kiwi.plugs.run('command_run', plugin_event);
@@ -270,29 +274,37 @@ kiwi.front = {
         // Update msg if it's been changed by any plugins
         msg = plugin_event.command.toString();
 
-        console.log("running " + msg);
+
         if (msg.substring(0, 1) === '/') {
+            console.log("running " + msg);
             parts = msg.split(' ');
             switch (parts[0].toLowerCase()) {
             case '/j':
             case '/join':
                 kiwi.front.joinChannel(parts[1]);
                 break;
-                
+
             case '/connect':
             case '/server':
-                if (parts[1] === undefined) {
-                    alert('Usage: /connect servername [port]');
+                if (typeof parts[1] === 'undefined') {
+                    alert('Usage: /connect servername [port] [ssl]');
                     break;
                 }
-                
-                if (parts[2] === undefined) {
+
+                if (typeof parts[2] === 'undefined') {
                     parts[2] = 6667;
                 }
-                kiwi.front.cur_channel.addMsg(null, ' ', '=== Connecting to ' + parts[1] + '...', 'status');
-                kiwi.gateway.connect(parts[1], parts[2], 0);
+
+                if ((typeof parts[3] === 'undefined') || !parts[3] || (parts[3] === 'false') || (parts[3] === 'no')) {
+                    parts[3] = false;
+                } else {
+                    parts[3] = true;
+                }
+
+                Tabview.getCurrentTab().addMsg(null, ' ', '=== Connecting to ' + parts[1] + ' on port ' + parts[2] + (parts[3] ? ' using SSL' : '') + '...', 'status');
+                kiwi.gateway.connect(parts[1], parts[2], parts[3]);
                 break;
-                
+
             case '/nick':
                 console.log("/nick");
                 if (parts[1] === undefined) {
@@ -306,63 +318,65 @@ kiwi.front = {
 
             case '/part':
                 if (typeof parts[1] === "undefined") {
-                    if (kiwi.front.cur_channel.safe_to_close) {
-                        kiwi.front.cur_channel.close();
+                    if (Tabview.getCurrentTab().safe_to_close) {
+                        Tabview.getCurrentTab().close();
                     } else {
-                        kiwi.gateway.raw(msg.substring(1) + ' ' + kiwi.front.cur_channel.name);
+                        kiwi.gateway.raw(msg.substring(1) + ' ' + Tabview.getCurrentTab().name);
                     }
                 } else {
                     kiwi.gateway.raw(msg.substring(1));
                 }
                 break;
-                
+
             case '/names':
                 if (typeof parts[1] !== "undefined") {
                     kiwi.gateway.raw(msg.substring(1));
                 }
                 break;
-                
+
             case '/debug':
                 kiwi.gateway.debug();
                 break;
-                
+
             case '/q':
             case '/query':
                 if (typeof parts[1] !== "undefined") {
-                    kiwi.front.tabviewAdd(parts[1]);
+                    tab = new Tabview(parts[1]);
                 }
                 break;
 
-            
+
             case '/m':
             case '/msg':
                 if (typeof parts[1] !== "undefined") {
                     msg_sliced = msg.split(' ').slice(2).join(' ');
                     kiwi.gateway.msg(parts[1], msg_sliced);
 
-                    if (!kiwi.front.tabviewExists(parts[1])) {
-                        kiwi.front.tabviewAdd(parts[1]);
+                    tab = Tabview.getTab(parts[1]);
+                    if (!tab) {
+                        tab = new Tabview(parts[1]);
                     }
-                    kiwi.front.tabviews[parts[1].toLowerCase()].addMsg(null, kiwi.gateway.nick, msg_sliced);
+                    tab.addMsg(null, kiwi.gateway.nick, msg_sliced);
                 }
                 break;
-            
+
             case '/k':
             case '/kick':
                 if (typeof parts[1] === 'undefined') {
                     return;
                 }
-                kiwi.gateway.raw('KICK ' + kiwi.front.cur_channel.name + ' ' + msg.split(' ', 2)[1]);
+                kiwi.gateway.raw('KICK ' + Tabview.getCurrentTab().name + ' ' + msg.split(' ', 2)[1]);
                 break;
 
             case '/quote':
                 kiwi.gateway.raw(msg.replace(/^\/quote /i, ''));
                 break;
-                
+
             case '/me':
-                kiwi.gateway.action(kiwi.front.cur_channel.name, msg.substring(4));
-                //kiwi.front.tabviews[destination.toLowerCase()].addMsg(null, ' ', '* '+data.nick+' '+data.msg, 'color:green;');
-                kiwi.front.cur_channel.addMsg(null, ' ', '* ' + kiwi.gateway.nick + ' ' + msg.substring(4), 'action', 'color:#555;');
+                tab = Tabview.getCurrentTab();
+                kiwi.gateway.action(tab.name, msg.substring(4));
+                //tab.addMsg(null, ' ', '* '+data.nick+' '+data.msg, 'color:green;');
+                tab.addMsg(null, ' ', '* ' + kiwi.gateway.nick + ' ' + msg.substring(4), 'action', 'color:#555;');
                 break;
 
             case '/notice':
@@ -380,7 +394,7 @@ kiwi.front = {
                 break;
 
             case '/quit':
-                kiwi.gateway.quit(msg.split(" ", 2)[1]);
+                kiwi.gateway.quit(parts.slice(1).join(' '));
                 break;
 
             case '/topic':
@@ -397,17 +411,25 @@ kiwi.front = {
                         t.setSelectionRange(pos, pos);
                     }
                 } else {
-                    kiwi.gateway.setTopic(kiwi.front.cur_channel.name, msg.split(' ', 2)[1]);
-                    //kiwi.gateway.raw('TOPIC ' + kiwi.front.cur_channel.name + ' :' + msg.split(' ', 2)[1]);
+                    kiwi.gateway.setTopic(Tabview.getCurrentTab().name, msg.split(' ', 2)[1]);
+                    //kiwi.gateway.raw('TOPIC ' + Tabview.getCurrentTab().name + ' :' + msg.split(' ', 2)[1]);
                 }
                 break;
 
             case '/kiwi':
-                kiwi.gateway.kiwi(kiwi.front.cur_channel.name, msg.substring(6));
+                kiwi.gateway.kiwi(Tabview.getCurrentTab().name, msg.substring(6));
                 break;
 
+            case '/ctcp':
+                parts = parts.slice(1);
+                dest = parts.shift();
+                msg = parts.join(' ');
+
+                kiwi.gateway.msg(dest, String.fromCharCode(1) + msg + String.fromCharCode(1));
+                Tabview.getServerTab().addMsg(null, 'CTCP Request', '[to ' + dest + '] ' + msg, 'ctcp');
+                break;
             default:
-                //kiwi.front.cur_channel.addMsg(null, ' ', '--> Invalid command: '+parts[0].substring(1));
+                //Tabview.getCurrentTab().addMsg(null, ' ', '--> Invalid command: '+parts[0].substring(1));
                 kiwi.gateway.raw(msg.substring(1));
             }
 
@@ -416,79 +438,83 @@ kiwi.front = {
             if (msg.trim() === '') {
                 return;
             }
-            if (kiwi.front.cur_channel.name !== 'server') {
-                kiwi.gateway.msg(kiwi.front.cur_channel.name, msg);
+            if (Tabview.getCurrentTab().name !== 'server') {
+                kiwi.gateway.msg(Tabview.getCurrentTab().name, msg);
                 d = new Date();
                 d = d.getHours() + ":" + d.getMinutes();
                 //kiwi.front.addMsg(d, kiwi.gateway.nick, msg);
-                kiwi.front.cur_channel.addMsg(null, kiwi.gateway.nick, msg);
+                Tabview.getCurrentTab().addMsg(null, kiwi.gateway.nick, msg);
             }
         }
     },
-    
-    
+
+
     onMsg: function (e, data) {
-        var destination, plugin_event;
+        var destination, plugin_event, tab;
         // Is this message from a user?
         if (data.channel === kiwi.gateway.nick) {
             destination = data.nick.toLowerCase();
         } else {
-            destination = data.channel.toLowerCase();    
+            destination = data.channel.toLowerCase();
         }
-        
+
         plugin_event = {nick: data.nick, msg: data.msg, destination: destination};
         plugin_event = kiwi.plugs.run('msg_recieved', plugin_event);
         if (!plugin_event) {
             return;
         }
-
-        if (!kiwi.front.tabviewExists(plugin_event.destination)) {
-            kiwi.front.tabviewAdd(plugin_event.destination);
+        tab = Tabview.getTab(plugin_event.destination);
+        if (!tab) {
+            tab = new Tabview(plugin_event.destination);
         }
-        kiwi.front.tabviews[plugin_event.destination].addMsg(null, plugin_event.nick, plugin_event.msg);
+        tab.addMsg(null, plugin_event.nick, plugin_event.msg);
     },
-    
+
     onDebug: function (e, data) {
-        if (!kiwi.front.tabviewExists('kiwi_debug')) {
-            kiwi.front.tabviewAdd('kiwi_debug');
+        var tab = Tabview.getTab('kiwi_debug');
+        if (!tab) {
+            tab = new Tabview('kiwi_debug');
         }
-        kiwi.front.tabviews.kiwi_debug.addMsg(null, ' ', data.msg);
+        tab.addMsg(null, ' ', data.msg);
     },
-    
+
     onAction: function (e, data) {
-        var destination;
+        var destination, tab;
         // Is this message from a user?
         if (data.channel === kiwi.gateway.nick) {
             destination = data.nick;
         } else {
-            destination = data.channel;    
+            destination = data.channel;
         }
-        
-        if (!kiwi.front.tabviewExists(destination)) {
-            kiwi.front.tabviewAdd(destination);
+
+        tab = Tabview.getTab(destination);
+        if (!tab) {
+            tab = new Tabview(destination);
         }
-        kiwi.front.tabviews[destination.toLowerCase()].addMsg(null, ' ', '* ' + data.nick + ' ' + data.msg, 'action', 'color:#555;');
+        tab.addMsg(null, ' ', '* ' + data.nick + ' ' + data.msg, 'action', 'color:#555;');
     },
-    
+
     onTopic: function (e, data) {
-        if (kiwi.front.tabviewExists(data.channel)) {
-            kiwi.front.tabviews[data.channel.toLowerCase()].changeTopic(data.topic);            
+        var tab = Tabview.getTab(data.channel);
+        if (tab) {
+            tab.changeTopic(data.topic);
         }
     },
-    
+
     onNotice: function (e, data) {
         var nick = (data.nick === undefined) ? '' : data.nick,
-            enick = '[' + nick + ']';
+            enick = '[' + nick + ']',
+            tab;
 
-        if (kiwi.front.tabviewExists(data.target)) {
-            kiwi.front.tabviews[data.target.toLowerCase()].addMsg(null, enick, data.msg, 'notice');
-        } else if (kiwi.front.tabviewExists(nick)) {
-            kiwi.front.tabviews[nick.toLowerCase()].addMsg(null, enick, data.msg, 'notice');
+        if (Tabview.tabExists(data.target)) {
+            Tabview.getTab(data.target).addMsg(null, enick, data.msg, 'notice');
+        } else if (Tabview.tabExists(nick)) {
+            Tabview.getTab(nick).addMsg(null, enick, data.msg, 'notice');
         } else {
-            kiwi.front.tabviews.server.addMsg(null, enick, data.msg, 'notice');
+            Tabview.getServerTab().addMsg(null, enick, data.msg, 'notice');
         }
     },
-    
+
     onCTCPRequest: function (e, data) {
         var msg = data.msg.split(" ", 2);
         switch (msg[0]) {
@@ -502,137 +528,169 @@ kiwi.front = {
             kiwi.gateway.notice(data.nick, String.fromCharCode(1) + 'TIME ' + (new Date()).toLocaleString() + String.fromCharCode(1));
             break;
         }
-        kiwi.front.tabviews.server.addMsg(null, 'CTCP [' + data.nick + ']', data.msg, 'ctcp');
+        Tabview.getServerTab().addMsg(null, 'CTCP Request', '[from ' + data.nick + '] ' + data.msg, 'ctcp');
     },
-    
+
     onCTCPResponse: function (e, data) {
+        Tabview.getServerTab().addMsg(null, 'CTCP Reply', '[from ' + data.nick + '] ' + data.msg, 'ctcp');
     },
 
     onKiwi: function (e, data) {
         //console.log(data);
     },
-    
+
     onConnect: function (e, data) {
+        var err_box, channels;
+
         if (data.connected) {
+            // Did we disconnect?
+            err_box = $('.messages .msg.error.disconnect .text');
+            if (typeof err_box[0] !== 'undefined') {
+                err_box.text('Reconnected OK :)');
+                err_box.parent().removeClass('disconnect');
+
+                // Rejoin channels
+                channels = '';
+                _.each(Tabview.getAllTabs(), function (tabview) {
+                    if (tabview.name === 'server') {
+                        return;
+                    }
+                    channels += tabview.name + ',';
+                });
+                console.log('Rejoining: ' + channels);
+                kiwi.gateway.join(channels);
+                return;
+            }
+
             if (kiwi.gateway.nick !== data.nick) {
                 kiwi.gateway.nick = data.nick;
                 kiwi.front.doLayout();
             }
 
-            kiwi.front.tabviews.server.addMsg(null, ' ', '=== Connected OK :)', 'status');
+            Tabview.getServerTab().addMsg(null, ' ', '=== Connected OK :)', 'status');
             if (typeof init_data.channel === "string") {
                 kiwi.front.joinChannel(init_data.channel);
             }
             kiwi.plugs.run('connect', {success: true});
         } else {
-            kiwi.front.tabviews.server.addMsg(null, ' ', '=== Failed to connect :(', 'status');
+            Tabview.getServerTab().addMsg(null, ' ', '=== Failed to connect :(', 'status');
             kiwi.plugs.run('connect', {success: false});
         }
     },
     onConnectFail: function (e, data) {
         var reason = (typeof data.reason === 'string') ? data.reason : '';
-        kiwi.front.tabviews.server.addMsg(null, '', 'There\'s a problem connecting! (' + reason + ')', 'error');
+        Tabview.getServerTab().addMsg(null, '', 'There\'s a problem connecting! (' + reason + ')', 'error');
         kiwi.plugs.run('connect', {success: false});
     },
     onDisconnect: function (e, data) {
-        var tab;
-        for (tab in kiwi.front.tabviews) {
-            kiwi.front.tabviews[tab].addMsg(null, '', 'Disconnected from server!', 'error');
+        var tab, tabs;
+        tabs = Tabview.getAllTabs();
+        for (tab in tabs) {
+            tabs[tab].addMsg(null, '', 'Disconnected from server!', 'error disconnect');
         }
         kiwi.plugs.run('disconnect', {success: false});
     },
+    onReconnecting: function (e, data) {
+        var err_box, f, msg;
+
+        err_box = $('.messages .msg.error.disconnect .text');
+        if (!err_box) {
+            return;
+        }
+
+        f = function (num) {
+            switch (num) {
+            case 1: return 'First';
+            case 2: return 'Second';
+            case 3: return 'Third';
+            case 4: return 'Fourth';
+            case 5: return 'Fifth';
+            case 6: return 'Sixth';
+            case 7: return 'Seventh';
+            default: return 'Next';
+            }
+        };
+
+        // TODO: convert seconds to mins:secs
+        msg = f(data.attempts) + ' attempt at reconnecting in ' + (data.delay / 1000).toString() + ' seconds..';
+        err_box.text(msg);
+    },
     onOptions: function (e, data) {
         if (typeof kiwi.gateway.network_name === "string" && kiwi.gateway.network_name !== "") {
-            kiwi.front.tabviews.server.tab.text(kiwi.gateway.network_name);
+            Tabview.getServerTab().tab.text(kiwi.gateway.network_name);
         }
     },
     onMOTD: function (e, data) {
-        kiwi.front.tabviews.server.addMsg(null, data.server, data.msg, 'motd');
+        Tabview.getServerTab().addMsg(null, data.server, data.msg, 'motd');
     },
     onWhois: function (e, data) {
-        var d;
+        var d, tab;
+        tab = Tabview.getCurrentTab();
         if (data.msg) {
-            kiwi.front.cur_channel.addMsg(null, data.nick, data.msg, 'whois');
+            tab.addMsg(null, data.nick, data.msg, 'whois');
         } else if (data.logon) {
             d = new Date();
             d.setTime(data.logon * 1000);
             d = d.toLocaleString();
-            kiwi.front.cur_channel.addMsg(null, data.nick, 'idle for ' + data.idle + ' second' + ((data.idle !== 1) ? 's' : '') + ', signed on ' + d, 'whois');
+            tab.addMsg(null, data.nick, 'idle for ' + data.idle + ' second' + ((data.idle !== 1) ? 's' : '') + ', signed on ' + d, 'whois');
         } else {
-            kiwi.front.cur_channel.addMsg(null, data.nick, 'idle for ' + data.idle + ' seconds', 'whois');
+            tab.addMsg(null, data.nick, 'idle for ' + data.idle + ' seconds', 'whois');
         }
     },
     onMode: function (e, data) {
-        var i, new_nick_text;
-        
-        // TODO: Store the modes in the elements data, then work out the current
-        // mode symbol from the highest mode. Eg. -h may leave +o from previous modes; It
-        // doesn't simply clear it! ~Darren
-        if (typeof data.channel === 'string' && typeof data.effected_nick === 'string') {
-            kiwi.front.tabviews[data.channel.toLowerCase()].addMsg(null, ' ', '[' + data.mode + '] ' + data.effected_nick + ' by ' + data.nick, 'mode', '');
-            kiwi.front.tabviews[data.channel.toLowerCase()].userlist.children().each(function () {
-                if (kiwi.front.nickStripPrefix($(this).text()) === data.effected_nick) {
-
-                    if (data.mode.split('')[0] === '+') {
-                        for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) {
-                            if (kiwi.gateway.user_prefixes[i].mode === data.mode.split('')[1]) {
-                                new_nick_text = kiwi.gateway.user_prefixes[i].symbol + data.effected_nick;
-                                break;
-                            }
-                        }
-                    } else if (data.mode.split('')[0] === '-') {
-                        new_nick_text = data.effected_nick;
-                    }
-
-                    if (new_nick_text !== '') {
-                        $(this).text(new_nick_text);
-                        return false;
-                    }
-
-                }
-            });
+        var tab;
+        if ((typeof data.channel === 'string') && (typeof data.effected_nick === 'string')) {
+            tab = Tabview.getTab(data.channel);
+            tab.addMsg(null, ' ', '[' + data.mode + '] ' + data.effected_nick + ' by ' + data.nick, 'mode', '');
+            if (tab.userlist.hasUser(data.effected_nick)) {
+                tab.userlist.changeUserMode(data.effected_nick, data.mode.substr(1), (data.mode[0] === '+'));
+            }
         }
+
+        // TODO: Other mode changes that aren't +/- qaohv. - JA
     },
     onUserList: function (e, data) {
-        var ul, nick, mode;
-        if (kiwi.front.tabviews[data.channel.toLowerCase()] === undefined) {
+        var tab;
+
+        tab = Tabview.getTab(data.channel);
+        if (!tab) {
             return;
         }
-        ul = kiwi.front.tabviews[data.channel.toLowerCase()].userlist;
-        
-        if (!document.userlist_updating) {
-            document.userlist_updating = true;
-            ul.empty();
+
+        if ((!kiwi.front.cache.userlist) || (!kiwi.front.cache.userlist.updating)) {
+            if (!kiwi.front.cache.userlist) {
+                kiwi.front.cache.userlist = {updating: true};
+            } else {
+                kiwi.front.cache.userlist.updating = true;
+            }
+            tab.userlist.empty();
         }
-        
-        $.each(data.users, function (i, item) {
-            nick = i; //i.match(/^.+!/g);
-            mode = item;
-            $('<li><a class="nick" onclick="kiwi.front.userClick(this);">' + mode + nick + '</a></li>').appendTo(ul);
-        });
-        
-        kiwi.front.tabviews[data.channel.toLowerCase()].userlistSort();
+
+        tab.userlist.addUser(data.users);
+
     },
     onUserListEnd: function (e, data) {
-        document.userlist_updating = false;
+        if (!kiwi.front.cache.userlist) {
+            kiwi.front.cache.userlist = {};
+        }
+        kiwi.front.cache.userlist.updating = false;
     },
-    
+
     onChannelListStart: function (e, data) {
+        /*global Utilityview */
         var tab, table;
-        console.log('/list start');
-        console.profile('list');
-        
+
         tab = new Utilityview('Channel List');
         tab.div.css('overflow-y', 'scroll');
-        table = $('<table><thead style="font-weight: bold;"><tr><td>Channel Name</td><td>Members</td><td style="padding-left: 2em;">Topic</td></tr></thead><tbody style="vertical-align: top;"></tbody>');
+        table = $('<table style="margin:1em 2em;"><thead style="font-weight: bold;"><tr><td>Channel Name</td><td>Members</td><td style="padding-left: 2em;">Topic</td></tr></thead><tbody style="vertical-align: top;"></tbody>');
         tab.div.append(table);
-        
+
         kiwi.front.cache.list = {chans: [], tab: tab, table: table,
             update: function (newChans) {
                 var body = this.table.children('tbody:first').detach(),
                     chan,
                     html;
-                
+
                 html = '';
                 for (chan in newChans) {
                     this.chans.push(newChans[chan]);
@@ -640,95 +698,88 @@ kiwi.front = {
                 }
                 body.append(html);
                 this.table.append(body);
-                
+
             },
             finalise: function () {
                 var body = this.table.children('tbody:first').detach(),
                     list,
                     chan;
-                
+
                 list = $.makeArray($(body).children());
-                
+
                 for (chan in list) {
                     list[chan] = $(list[chan]).detach();
                 }
-                
+
                 list = _.sortBy(list, function (channel) {
                     return parseInt(channel.children('.num_users').first().text(), 10);
                 }).reverse();
-                
+
                 for (chan in list) {
                     body.append(list[chan]);
                 }
-                
+
                 this.table.append(body);
-                
+
             }};
     },
     onChannelList: function (e, data) {
         var chans;
-        console.log(data);
         data = data.chans;
         //data = [data];
         for (chans in data) {
-            data[chans] = {data: data[chans], html: '<tr><td><a class="chan">' + data[chans].channel + '</a></td><td class="num_users" style="text-align: center;">' + data[chans].num_users + '</td><td style="padding-left: 2em;">' + kiwi.front.format(data[chans].topic) + '</td></tr>'};
+            data[chans] = {data: data[chans], html: '<tr><td><a class="chan">' + data[chans].channel + '</a></td><td class="num_users" style="text-align: center;">' + data[chans].num_users + '</td><td style="padding-left: 2em;">' + kiwi.front.formatIRCMsg(data[chans].topic) + '</td></tr>'};
         }
         kiwi.front.cache.list.update(data);
     },
     onChannelListEnd: function (e, data) {
         kiwi.front.cache.list.finalise();
         kiwi.front.cache.list.tab.show();
-        console.profileEnd();
-        console.log('/list end.');
     },
 
 
     onJoin: function (e, data) {
-        if (!kiwi.front.tabviewExists(data.channel)) {
-            kiwi.front.tabviewAdd(data.channel.toLowerCase());
+        var tab = Tabview.getTab(data.channel);
+        if (!tab) {
+            tab = new Tabview(data.channel.toLowerCase());
         }
-        
+
+        tab.addMsg(null, ' ', '--> ' + data.nick + ' has joined', 'action join', 'color:#009900;');
+
         if (data.nick === kiwi.gateway.nick) {
             return; // Not needed as it's already in nicklist
         }
-        kiwi.front.tabviews[data.channel.toLowerCase()].addMsg(null, ' ', '--> ' + data.nick + ' has joined', 'action join', 'color:#009900;');
-        $('<li><a class="nick" onclick="kiwi.front.userClick(this);">' + data.nick + '</a></li>').appendTo(kiwi.front.tabviews[data.channel.toLowerCase()].userlist);
-        kiwi.front.tabviews[data.channel.toLowerCase()].userlistSort();
+
+        tab.userlist.addUser({nick: data.nick, modes: []});
     },
     onPart: function (e, data) {
-        if (kiwi.front.tabviewExists(data.channel)) {
+        var tab = Tabview.getTab(data.channel);
+        if (tab) {
             // If this is us, close the tabview
             if (data.nick === kiwi.gateway.nick) {
-                kiwi.front.tabviews[data.channel.toLowerCase()].close();
-                kiwi.front.tabviews.server.show();
+                tab.close();
+                Tabview.getServerTab().show();
                 return;
             }
-            
-            kiwi.front.tabviews[data.channel.toLowerCase()].addMsg(null, ' ', '<-- ' + data.nick + ' has left (' + data.message + ')', 'action part', 'color:#990000;');
-            kiwi.front.tabviews[data.channel.toLowerCase()].userlist.children().each(function () {
-                if ($(this).text() === data.nick) {
-                    $(this).remove();
-                }
-            });
+
+            tab.addMsg(null, ' ', '<-- ' + data.nick + ' has left (' + data.message + ')', 'action part', 'color:#990000;');
+            tab.userlist.removeUser(data.nick);
         }
     },
     onKick: function (e, data) {
-        if (kiwi.front.tabviewExists(data.channel)) {
+        var tab = Tabview.getTab(data.channel);
+        if (tab) {
             // If this is us, close the tabview
             if (data.kicked === kiwi.gateway.nick) {
-                //kiwi.front.tabviews[data.channel.toLowerCase()].close();
-                kiwi.front.tabviews[data.channel.toLowerCase()].addMsg(null, ' ', '=== You have been kicked from ' + data.channel + '. ' + data.message, 'status kick');
-                kiwi.front.tabviews[data.channel.toLowerCase()].safe_to_close = true;
-                $('li', kiwi.front.tabviews[data.channel.toLowerCase()].userlist).remove();
+                //tab.close();
+                tab.addMsg(null, ' ', '=== You have been kicked from ' + data.channel + '. ' + data.message, 'status kick');
+                tab.safe_to_close = true;
+                tab.userlist.remove();
                 return;
             }
-            
-            kiwi.front.tabviews[data.channel.toLowerCase()].addMsg(null, ' ', '<-- ' + data.kicked + ' kicked by ' + data.nick + '(' + data.message + ')', 'action kick', 'color:#990000;');
-            kiwi.front.tabviews[data.channel.toLowerCase()].userlist.children().each(function () {
-                if ($(this).text() === data.nick) {
-                    $(this).remove();
-                }
-            });
+
+            tab.addMsg(null, ' ', '<-- ' + data.kicked + ' kicked by ' + data.nick + '(' + data.message + ')', 'action kick', 'color:#990000;');
+            tab.userlist.removeUser(data.nick);
         }
     },
     onNick: function (e, data) {
@@ -736,87 +787,94 @@ kiwi.front = {
             kiwi.gateway.nick = data.newnick;
             kiwi.front.doLayout();
         }
-        
-        $.each(kiwi.front.tabviews, function (i, item) {
-            $.each(kiwi.front.tabviews, function (i, item) {
-                item.changeNick(data.newnick, data.nick);
-            });
+
+        _.each(Tabview.getAllTabs(), function (tab) {
+            if (tab.userlist.hasUser(data.nick)) {
+                tab.userlist.renameUser(data.nick, data.newnick);
+                tab.addMsg(null, ' ', '=== ' + data.nick + ' is now known as ' + data.newnick, 'action changenick');
+            }
         });
     },
     onQuit: function (e, data) {
-        $.each(kiwi.front.tabviews, function (i, item) {
-            $.each(kiwi.front.tabviews, function (i, item) {
-                item.userlist.children().each(function () {
-                    if ($(this).text() === data.nick) {
-                        $(this).remove();
-                        item.addMsg(null, ' ', '<-- ' + data.nick + ' has quit (' + data.message + ')', 'action quit', 'color:#990000;');
-                    }
-                });
-            });
+        _.each(Tabview.getAllTabs(), function (tab) {
+            if (tab.userlist.hasUser(data.nick)) {
+                tab.userlist.removeUser(data.nick);
+                tab.addMsg(null, ' ', '<-- ' + data.nick + ' has quit (' + data.message + ')', 'action quit', 'color:#990000;');
+            }
         });
     },
     onChannelRedirect: function (e, data) {
-        kiwi.front.tabviews[data.from.toLowerCase()].close();
-        kiwi.front.tabviewAdd(data.to.toLowerCase());
-        kiwi.front.tabviews[data.to.toLowerCase()].addMsg(null, ' ', '=== Redirected from ' + data.from, 'action');
+        var tab = Tabview.getTab(data.from);
+        tab.close();
+        tab = new Tabview(data.to);
+        tab.addMsg(null, ' ', '=== Redirected from ' + data.from, 'action');
     },
-    
+
     onIRCError: function (e, data) {
-        var t_view;
-        if (data.channel !== undefined && kiwi.front.tabviewExists(data.channel)) {
+        var t_view,
+            tab = Tabview.getTab(data.channel);
+        if (data.channel !== undefined && tab) {
             t_view = data.channel;
         } else {
             t_view = 'server';
+            tab = Tabview.getServerTab();
         }
 
         switch (data.error) {
         case 'banned_from_channel':
-            kiwi.front.tabviews[t_view].addMsg(null, ' ', '=== You are banned from ' + data.channel + '. ' + data.reason, 'status');
+            tab.addMsg(null, ' ', '=== You are banned from ' + data.channel + '. ' + data.reason, 'status');
             if (t_view !== 'server') {
-                kiwi.front.tabviews[t_view].safe_to_close = true;
+                tab.safe_to_close = true;
             }
             break;
         case 'bad_channel_key':
-            kiwi.front.tabviews[t_view].addMsg(null, ' ', '=== Bad channel key for ' + data.channel, 'status');
+            tab.addMsg(null, ' ', '=== Bad channel key for ' + data.channel, 'status');
             if (t_view !== 'server') {
-                kiwi.front.tabviews[t_view].safe_to_close = true;
+                tab.safe_to_close = true;
             }
             break;
         case 'invite_only_channel':
-            kiwi.front.tabviews[t_view].addMsg(null, ' ', '=== ' + data.channel + ' is invite only.', 'status');
+            tab.addMsg(null, ' ', '=== ' + data.channel + ' is invite only.', 'status');
             if (t_view !== 'server') {
-                kiwi.front.tabviews[t_view].safe_to_close = true;
+               tab.safe_to_close = true;
             }
             break;
         case 'channel_is_full':
-            kiwi.front.tabviews[t_view].addMsg(null, ' ', '=== ' + data.channel + ' is full.', 'status');
+            tab.addMsg(null, ' ', '=== ' + data.channel + ' is full.', 'status');
             if (t_view !== 'server') {
-                kiwi.front.tabviews[t_view].safe_to_close = true;
+                tab.safe_to_close = true;
             }
             break;
         case 'chanop_privs_needed':
-            kiwi.front.tabviews[data.channel].addMsg(null, ' ', '=== ' + data.reason, 'status');
+            tab.addMsg(null, ' ', '=== ' + data.reason, 'status');
             break;
         case 'no_such_nick':
-            kiwi.front.tabviews.server.addMsg(null, ' ', '=== ' + data.nick + ': ' + data.reason, 'status'); 
+            Tabview.getServerTab().addMsg(null, ' ', '=== ' + data.nick + ': ' + data.reason, 'status');
             break;
         case 'nickname_in_use':
-            kiwi.front.tabviews.server.addMsg(null, ' ', '=== The nickname ' + data.nick + ' is already in use. Please select a new nickname', 'status');
-            kiwi.front.showChangeNick();
+            Tabview.getServerTab().addMsg(null, ' ', '=== The nickname ' + data.nick + ' is already in use. Please select a new nickname', 'status');
+            kiwi.front.showChangeNick('That nick is already taken');
             break;
         default:
             // We don't know what data contains, so don't do anything with it.
             //kiwi.front.tabviews.server.addMsg(null, ' ', '=== ' + data, 'status');
         }
     },
-    
+
     registerKeys: function () {
+        var tabcomplete = {active: false, data: [], prefix: ''};
         $('#kiwi_msginput').bind('keydown', function (e) {
-            var windows, meta, num, msg, data, candidates, word_pos, word, i;
+            var windows, meta, num, msg, data, self;
             windows = $('#windows');
             //var meta = e.altKey;
             meta = e.ctrlKey;
-            
+
+            if (e.which !== 9) {
+                tabcomplete.active = false;
+                tabcomplete.data = [];
+                tabcomplete.prefix = '';
+            }
+
             switch (true) {
             case (e.which >= 48) && (e.which <= 57):
                 if (meta) {
@@ -834,13 +892,13 @@ kiwi.front = {
             case e.which === 13:            // return
                 msg = $('#kiwi_msginput').val();
                 msg = msg.trim();
-                
+
                 kiwi.front.buffer.push(msg);
                 kiwi.front.buffer_pos = kiwi.front.buffer.length;
-                
+
                 kiwi.front.run(msg);
                 $('#kiwi_msginput').val('');
-                
+
                 break;
             case e.which === 33:             // page up
                 console.log("page up");
@@ -873,64 +931,83 @@ kiwi.front = {
                     $('#kiwi_msginput').val(kiwi.front.buffer[kiwi.front.buffer_pos]);
                 }
                 break;
-                
+
             case e.which === 9:                // tab
-                // Get possible autocompletions
-                data = [];
-                kiwi.front.cur_channel.userlist.children().each(function () {
-                    var nick;
-                    nick = kiwi.front.nickStripPrefix($('a.nick', this).text());
-                    data.push(nick);
-                });
-                
-                // Do the autocomplete
-                if (this.value.length === this.selectionStart && this.value.length === this.selectionEnd) {
-                    candidates = [];
-                    
-                    word_pos = this.value.lastIndexOf(' ');
-                    word = "";
-                    if (word_pos === -1) {
-                        word = this.value;
-                    } else {
-                        word = this.value.substr(word_pos);
+                tabcomplete.active = true;
+                if (_.isEqual(tabcomplete.data, [])) {
+                    // Get possible autocompletions
+                    data = [];
+                    Tabview.getCurrentTab().userlist.listUsers(false).each(function () {
+                        var nick;
+                        nick = kiwi.front.nickStripPrefix($('a.nick', this).text());
+                        data.push(nick);
+                    });
+                    data = _.sortBy(data, function (nick) {
+                        return nick;
+                    });
+                    tabcomplete.data = data;
+                }
+
+                if (this.value[this.selectionStart - 1] === ' ') {
+                    return false;
+                }
+                self = this;
+                (function () {
+                    var tokens = self.value.substring(0, self.selectionStart).split(" "),
+                        val,
+                        p1,
+                        newnick,
+                        range,
+                        nick = tokens[tokens.length - 1];
+                    if (tabcomplete.prefix === '') {
+                        tabcomplete.prefix = nick;
                     }
-                    word = word.trim();
-                    
-                    // filter data to find only strings that start with existing value
-                    for (i = 0; i < data.length; i++) {
-                        if (data[i].indexOf(word) === 0 && data[i].length > word.length) {
-                            candidates.push(data[i]);
+
+                    tabcomplete.data = _.select(tabcomplete.data, function (n) {
+                        return (n.toLowerCase().indexOf(tabcomplete.prefix.toLowerCase()) === 0);
+                    });
+
+                    if (tabcomplete.data.length > 0) {
+                        p1 = self.selectionStart - (nick.length);
+                        val = self.value.substr(0, p1);
+                        newnick = tabcomplete.data.shift();
+                        tabcomplete.data.push(newnick);
+                        val += newnick;
+                        val += self.value.substr(self.selectionStart);
+                        self.value = val;
+                        if (self.setSelectionRange) {
+                            self.setSelectionRange(p1 + newnick.length, p1 + newnick.length);
+                        } else if (self.createTextRange) { // not sure if this bit is actually needed....
+                            range = self.createTextRange();
+                            range.collapse(true);
+                            range.moveEnd('character', p1 + newnick.length);
+                            range.moveStart('character', p1 + newnick.length);
+                            range.select();
                         }
                     }
-                    
-                    if (candidates.length > 0) {
-                        // some candidates for autocompletion are found
-                        this.value = this.value.substring(0, word_pos) + ' ' + candidates[0] + ': ';
-                        this.selectionStart = this.value.length;
-                    }
-                }
+                }());
                 return false;
             }
         });
-        
-        
+
+
         $('#kiwi .control .msginput .nick').click(function () {
-            kiwi.front.showChangeNick();            
+            kiwi.front.showChangeNick();
         });
-        
-        
-        
-        
-        
+
+
+
+
+
         $('#kiwi .plugins .load_plugin_file').click(function () {
             if (typeof kiwi.front.boxes.plugins !== "undefined") {
                 return;
             }
-            
+
             kiwi.front.boxes.plugins = new Box("plugin_file");
             $('#tmpl_plugins').tmpl({}).appendTo(kiwi.front.boxes.plugins.content);
             kiwi.front.boxes.plugins.box.css('top', -(kiwi.front.boxes.plugins.height + 40));
-            
+
             // Populate the plugin list..
             function enumPlugins() {
                 var lst, j, txt;
@@ -942,7 +1019,7 @@ kiwi.front = {
                 }
             }
             enumPlugins();
-            
+
             // Event bindings
             $('#kiwi .plugin_file').submit(function () {
                 $('<div></div>').load($('.txtpluginfile').val(), function (e) {
@@ -953,18 +1030,18 @@ kiwi.front = {
             $('#kiwi .cancelpluginfile').click(function () {
                 kiwi.front.boxes.plugins.destroy();
             });
-            
+
             $('#kiwi #plugins_list_unload').click(function () {
                 var selected_plugin;
                 selected_plugin = $('#plugin_list').val();
                 kiwi.plugs.unloadPlugin(selected_plugin);
                 enumPlugins();
             });
-            
+
             $('#kiwi .txtpluginfile').focus();
-            
+
         });
-        
+
         $('#kiwi .plugins .reload_css').click(function () {
             var links = document.getElementsByTagName("link"),
                 i;
@@ -982,24 +1059,28 @@ kiwi.front = {
         $('#kiwi .about .about_close').click(function () {
             $('#kiwi .about').css('display', 'none');
         });
-        
-        
+
+
         $('#kiwi .poweredby').click(function () {
             $('#kiwi .about').css('display', 'block');
         });
-        
+
     },
-    
-    
-    showChangeNick: function () {
+
+
+    showChangeNick: function (caption) {
+        caption = (typeof caption !== 'undefined') ? caption : '';
+
         $('#kiwi').append($('#tmpl_change_nick').tmpl({}));
-        
+
+        $('#kiwi .newnick .caption').text(caption);
+
         $('#kiwi .form_newnick').submit(function () {
             kiwi.front.run('/NICK ' + $('#kiwi .txtnewnick').val());
             $('#kiwi .newnick').remove();
             return false;
         });
-        
+
         $('#kiwi .txtnewnick').keypress(function (ev) {
             if (!this.first_press) {
                 this.first_press = true;
@@ -1013,153 +1094,79 @@ kiwi.front = {
                 $('#kiwi .newnick').remove();
             }
         });
-        
+
         $('#kiwi .cancelnewnick').click(function () {
             $('#kiwi .newnick').remove();
         });
-        
-        $('#kiwi .txtnewnick').focus();
-    },
-
-
-    tabviewExists: function (name) {
-        return (typeof kiwi.front.tabviews[name.toLowerCase()] !== 'undefined');
-    },
-    
-    tabviewAdd: function (v_name) {
-        /*global Tabview */
-        var re, htmlsafe_name, tmp_divname, tmp_userlistname, tmp_tabname, userlist_enabled = true;
-
-        if (v_name.charAt(0) === kiwi.gateway.channel_prefix) {
-            re = new RegExp(kiwi.gateway.channel_prefix, "g");
-            htmlsafe_name = v_name.replace(re, 'pre');
-            htmlsafe_name = "chan_" + htmlsafe_name;
-        } else {
-            htmlsafe_name = 'query_' + v_name;
-            userlist_enabled = false;
-        }
-        
-        tmp_divname = 'kiwi_window_' + htmlsafe_name;
-        tmp_userlistname = 'kiwi_userlist_' + htmlsafe_name;
-        tmp_tabname = 'kiwi_tab_' + htmlsafe_name;
-        
-        if (!kiwi.front.tabviewExists(v_name)) {
-            $('#kiwi .windows .scroller').append('<div id="' + tmp_divname + '" class="messages"></div>');
-            $('#kiwi .userlist').append('<ul id="' + tmp_userlistname + '"></ul>');
-            $('#kiwi .windowlist ul').append('<li id="' + tmp_tabname + '" onclick="kiwi.front.tabviews[\'' + v_name.toLowerCase() + '\'].show();">' + v_name + '</li>');
-        }
-        //$('#kiwi .windowlist ul .window_'+v_name).click(function(){ kiwi.front.windowShow(v_name); });
-        //kiwi.front.windowShow(v_name);
-        
-        kiwi.front.tabviews[v_name.toLowerCase()] = new Tabview();
-        kiwi.front.tabviews[v_name.toLowerCase()].name = v_name;
-        kiwi.front.tabviews[v_name.toLowerCase()].div = $('#' + tmp_divname);
-        kiwi.front.tabviews[v_name.toLowerCase()].userlist = $('#' + tmp_userlistname);
-        kiwi.front.tabviews[v_name.toLowerCase()].tab = $('#' + tmp_tabname);
-        if (!userlist_enabled) {
-            kiwi.front.tabviews[v_name.toLowerCase()].userlist_width = 0;
-        }
-        kiwi.front.tabviews[v_name.toLowerCase()].show();
-        
-        if (typeof registerTouches === "function") {
-            //alert("Registering touch interface");
-            //registerTouches($('#'+tmp_divname));
-            registerTouches(document.getElementById(tmp_divname));
-        }
-        /*
-        kiwi.front.tabviews[v_name.toLowerCase()].userlist.click(function(){
-            alert($(this).attr('id'));
-        });
-        */
 
-        kiwi.front.doLayoutSize();
+        $('#kiwi .txtnewnick').focus();
     },
-    
-    
-    userClick: function (item) {
-        var li = $(item).parent();
 
-        // Remove any existing userboxes
-        $('#kiwi .userbox').remove();
 
-        if ($(li).data('userbox') === item) {
-            $(li).removeData('userbox');
-        } else {
-            $('#tmpl_user_box').tmpl({nick: kiwi.front.nickStripPrefix($(item).text())}).appendTo(li);
-            
-            $('#kiwi .userbox .userbox_query').click(function (ev) {
-                var nick = $('#kiwi .userbox_nick').val();
-                kiwi.front.run('/query ' + nick);
-            });
-            
-            $('#kiwi .userbox .userbox_whois').click(function (ev) {
-                var nick = $('#kiwi .userbox_nick').val();
-                kiwi.front.run('/whois ' + nick);
-            });
-            $(li).data('userbox', item);
-        }
-    },
-    
-    
     sync: function () {
         kiwi.gateway.sync();
     },
-    
+
     onSync: function (e, data) {
         // Set any settings
         if (data.nick !== undefined) {
             kiwi.gateway.nick = data.nick;
         }
-        
+
         // Add the tabviews
         if (data.tabviews !== undefined) {
-            $.each(data.tabviews, function (i, tab) {
-                if (!kiwi.front.tabviewExists(tab.name)) {
-                    kiwi.front.tabviewAdd(kiwi.gateway.channel_prefix + tab.name);
-                    
+            _.each(data.tabviews, function (tab) {
+                var newTab;
+                if (!Tabview.tabExists(tab.name)) {
+                    newTab = new Tabview(kiwi.gateway.channel_prefix + tab.name);
+
                     if (tab.userlist !== undefined) {
-                        kiwi.front.onUserList({'channel': kiwi.gateway.channel_prefix + tab.name, 'users': tab.userlist});
+                        kiwi.front.onUserList({'channel': kiwi.gateway.channel_prefix + tab.name, 'users': tab.userlist.getUsers(false)});
                     }
                 }
             });
         }
-        
+
         kiwi.front.doLayout();
     },
-    
-    
+
+
     setTopicText: function (new_topic) {
         kiwi.front.original_topic = new_topic;
-        $('#kiwi .cur_topic .topic').text(kiwi.front.format(new_topic));
+        $('#kiwi .cur_topic .topic').text(new_topic);
         kiwi.front.doLayoutSize();
     },
-    
+
     nickStripPrefix: function (nick) {
-        var tmp = nick, i, prefix;
-        
-        prefix = tmp.charAt(0);
-        for (i in kiwi.gateway.user_prefixes) {
-            if (kiwi.gateway.user_prefixes[i].symbol === prefix) {
-                return tmp.substring(1);
+        var tmp = nick, i, j, k;
+        i = 0;
+        for (j = 0; j < nick.length; j++) {
+            for (k = 0; k < kiwi.gateway.user_prefixes.length; k++) {
+                if (nick.charAt(j) === kiwi.gateway.user_prefixes[k].symbol) {
+                    i++;
+                    break;
+                }
             }
         }
 
-        return tmp;
+        return tmp.substr(i);
     },
-    
+
     nickGetPrefix: function (nick) {
-        var tmp = nick, i, prefix;
-        
-        prefix = tmp.charAt(0);
-        for (i in kiwi.gateway.user_prefixes) {
-            if (kiwi.gateway.user_prefixes[i].symbol === prefix) {
-                return prefix;
+        var tmp = nick, i, j, k;
+        i = 0;
+        for (j = 0; j < nick.length; j++) {
+            for (k = 0; k < kiwi.gateway.user_prefixes.length; k++) {
+                if (nick.charAt(j) === kiwi.gateway.user_prefixes[k].symbol) {
+                    i++;
+                    break;
+                }
             }
         }
 
-        return '';
+        return tmp.substr(0, i);
     },
-    
+
     isChannel: function (name) {
         var prefix, is_chan;
         prefix = name.charAt(0);
@@ -1168,16 +1175,16 @@ kiwi.front = {
         } else {
             is_chan = false;
         }
-        
+
         return is_chan;
     },
-    
+
     tabviewsNext: function () {
         var wl = $('#kiwi .windowlist ul'),
             next_left = parseInt(wl.css('text-indent').replace('px', ''), 10) + 170;
         wl.css('text-indent', next_left);
     },
-    
+
     tabviewsPrevious: function () {
         var wl = $('#kiwi .windowlist ul'),
             next_left = parseInt(wl.css('text-indent').replace('px', ''), 10) - 170;
@@ -1185,42 +1192,47 @@ kiwi.front = {
     },
 
     windowsNext: function () {
-        var tab, next;
+        var tab, tabs, curTab, next;
         next = false;
-        for (tab in kiwi.front.tabviews) {
+        tabs = Tabview.getAllTabs();
+        curTab = Tabview.getCurrentTab();
+        for (tab in tabs) {
             if (!next) {
-                if (kiwi.front.tabviews[tab] === kiwi.front.cur_channel) {
+                if (tabs[tab] === curTab) {
                     next = true;
                     continue;
                 }
             } else {
-                kiwi.front.tabviews[tab].show();
+                tabs[tab].show();
                 return;
             }
         }
     },
 
     windowsPrevious: function () {
-        var tab, prev_tab, next;
+        var tab, tabs, curTab, prev_tab, next;
         next = false;
-        for (tab in kiwi.front.tabviews) {
-            if (kiwi.front.tabviews[tab] === kiwi.front.cur_channel) {
+        tabs = Tabview.getAllTabs();
+        curTab = Tabview.getCurrentTab();
+        for (tab in tabs) {
+            if (tabs[tab] === curTab) {
                 if (prev_tab) {
                     prev_tab.show();
                 }
                 return;
             }
-            prev_tab = kiwi.front.tabviews[tab];
+            prev_tab = tabs[tab];
         }
     },
 
     windowsShow: function (num) {
         num = parseInt(num, 10);
         console.log('Showing window ' + num.toString());
-        var i = 0, tab;
-        for (tab in kiwi.front.tabviews) {
+        var i = 0, tab, tabs;
+        tabs = Tabview.getAllTabs();
+        for (tab in tabs) {
             if (i === num) {
-                kiwi.front.tabviews[tab].show();
+                tabs[tab].show();
                 return;
             }
             i++;
@@ -1238,10 +1250,14 @@ kiwi.front = {
         $('#kiwi .toolbars').slideUp();
         $('#kiwi .control').slideUp();
     },
-    
-    format: function (msg) {
-        var re;
-        
+
+    formatIRCMsg: function (msg) {
+        var re, next;
+
+        if ((!msg) || (typeof msg !== 'string')) {
+            return;
+        }
+
         // bold
         if (msg.indexOf(String.fromCharCode(2)) !== -1) {
             next = '<b>';
@@ -1253,7 +1269,7 @@ kiwi.front = {
                 msg = msg + '</b>';
             }
         }
-        
+
         // underline
         if (msg.indexOf(String.fromCharCode(31)) !== -1) {
             next = '<u>';
@@ -1265,10 +1281,10 @@ kiwi.front = {
                 msg = msg + '</u>';
             }
         }
-        
+
         // colour
         re = /\x03([0-9][0-5]?)(,([0-9][0-5]?))?(.*?)\x03/g;
-        
+
         msg = msg.replace(re, function (str, p1, p2, p3, p4) {
             var fg, bg,
                 col = function (num) {
@@ -1315,7 +1331,7 @@ kiwi.front = {
         });
         return msg;
     }
-    
+
 };
 
 
@@ -1327,7 +1343,287 @@ kiwi.front = {
 
 
 
+var UserList = function (name) {
+    var userlist, list_html, sortUsers, sortModes, getPrefix;
+
+    userlist = [];
 
+    $('#kiwi .userlist').append($('<ul id="kiwi_userlist_' + name + '"></ul>'));
+    list_html = $('#kiwi_userlist_' + name);
+    $('a.nick', list_html[0]).live('click', this.clickHandler);
+
+    sortUsers = function () {
+        var parent;
+        parent = list_html.parent();
+        list_html = list_html.detach();
+
+        // Not sure this is needed. 
+        // It's O(n^2) as well, so need to test to see if it works without.
+        // Alternative to test: list_html.children('li').detach();
+        list_html.children().each(function (child) {
+            var i, nick;
+            child = $(child);
+            nick = child.data('nick');
+            for (i = 0; i < userlist.length; i++) {
+                if (userlist[i].nick === nick) {
+                    userlist[i].html = child.detach();
+                    break;
+                }
+            }
+        });
+
+        userlist.sort(function (a, b) {
+            var i, a_idx, b_idx, a_nick, b_nick;
+            // Try to sort by modes first
+            if (a.modes.length > 0) {
+                // a has modes, but b doesn't so a should appear first
+                if (b.modes.length === 0) {
+                    return -1;
+                }
+                a_idx = b_idx = -1;
+                // Compare the first (highest) mode
+                for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) {
+                    if (kiwi.gateway.user_prefixes[i].mode === a.modes[0]) {
+                        a_idx = i;
+                    }
+                }
+                for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) {
+                    if (kiwi.gateway.user_prefixes[i].mode === b.modes[0]) {
+                        b_idx = i;
+                    }
+                }
+                if (a_idx < b_idx) {
+                    return -1;
+                } else if (a_idx > b_idx) {
+                    return 1;
+                }
+                // If we get to here both a and b have the same highest mode so have to resort to lexicographical sorting
+
+            } else if (b.modes.length > 0) {
+                // b has modes but a doesn't so b should appear first
+                return 1;
+            }
+            a_nick = a.nick.toLocaleUpperCase();
+            b_nick = b.nick.toLocaleUpperCase();
+            // Lexicographical sorting
+            if (a_nick < b_nick) {
+                return -1;
+            } else if (a_nick > b_nick) {
+                return 1;
+            } else {
+                // This should never happen; both users have the same nick.
+                console.log('Something\'s gone wrong somewhere - two users have the same nick!');
+                return 0;
+            }
+        });
+        _.each(userlist, function (user) {
+            user.html = user.html.appendTo(list_html);
+        });
+
+        list_html = list_html.appendTo(parent);
+    };
+
+    sortModes = function (modes) {
+        return modes.sort(function (a, b) {
+            var a_idx, b_idx, i;
+            for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) {
+                if (kiwi.gateway.user_prefixes[i].mode === a) {
+                    a_idx = i;
+                }
+            }
+            for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) {
+                if (kiwi.gateway.user_prefixes[i].mode === b) {
+                    b_idx = i;
+                }
+            }
+            if (a_idx < b_idx) {
+                return -1;
+            } else if (a_idx > b_idx) {
+                return 1;
+            } else {
+                return 0;
+            }
+        });
+    };
+
+    getPrefix = function (modes) {
+        var prefix = '';
+        if (typeof modes[0] !== 'undefined') {
+            prefix = _.detect(kiwi.gateway.user_prefixes, function (prefix) {
+                return prefix.mode === modes[0];
+            });
+            prefix = (prefix) ? prefix.symbol : '';
+        }
+        return prefix;
+    };
+
+    this.addUser = function (users) {
+        if (!_.isArray(users)) {
+            users = [users];
+        }
+        _.each(users, function (user) {
+            var html, prefix = '';
+            user.nick = kiwi.front.nickStripPrefix(user.nick);
+            user.modes = sortModes(user.modes);
+            if (typeof user.modes[0] !== 'undefined') {
+                prefix = _.detect(kiwi.gateway.user_prefixes, function (prefix) {
+                    return prefix.mode === user.modes[0];
+                });
+                prefix = (prefix) ? prefix.symbol : '';
+            }
+            html = $('<li><a class="nick">' + prefix + user.nick + '</a></li>');
+            userlist.push({nick: user.nick, modes: user.modes, html: html});
+        });
+        sortUsers();
+
+        return this;
+    };
+
+    this.removeUser = function (nicks) {
+        var toRemove;
+        if (!_.isArray(nicks)) {
+            nicks = [nicks];
+        }
+        toRemove = _.select(userlist, function (user) {
+            return _.any(nicks, function (n) {
+                return n === user.nick;
+            });
+        });
+
+        _.each(toRemove, function (user) {
+            user.html.remove();
+        });
+
+        userlist = _.difference(userlist, toRemove);
+
+        return this;
+    };
+
+    this.renameUser = function (oldNick, newNick) {
+        var user = _.detect(userlist, function (u) {
+            return u.nick === oldNick;
+        });
+        if (user) {
+            user.nick = newNick;
+            user.html.text(getPrefix(user.modes) + newNick);
+        }
+
+        sortUsers();
+
+        return this;
+    };
+
+    this.listUsers = function (modesort, modes) {
+        var users = userlist;
+        if (modes) {
+            users = _.select(users, function (user) {
+                return _.any(modes, function (m) {
+                    return _.any(user.modes, function (um) {
+                        return m === um;
+                    });
+                });
+            });
+        }
+        if ((modesort === true) || (typeof modesort === undefined)) {
+            return users;
+        } else {
+            return _.sortBy(users, function (user) {
+                return user.nick;
+            });
+        }
+    };
+
+    this.remove = function () {
+        list_html.remove();
+        list_html = null;
+        userlist = null;
+    };
+
+    this.empty = function () {
+        list_html.children().remove();
+        userlist = [];
+
+        return this;
+    };
+
+    this.hasUser = function (nick) {
+        return _.any(userlist, function (user) {
+            return user.nick === nick;
+        });
+    };
+
+    this.active = function (active) {
+        if ((arguments.length === 0) || (active)) {
+            list_html.addClass('active');
+            list_html.show();
+        } else {
+            list_html.removeClass('active');
+            list_html.hide();
+        }
+
+        return this;
+    };
+
+    this.changeUserMode = function (nick, mode, add) {
+        var user;
+        if (this.hasUser(nick)) {
+            user  = _.detect(userlist, function (u) {
+                return u.nick === nick;
+            });
+
+            if ((arguments.length < 3) || (add)) {
+                user.modes.push(mode);
+            } else {
+                user.modes = _.reject(user.modes, function (m) {
+                    return m === mode;
+                });
+            }
+            user.modes = sortModes(user.modes);
+            user.html.children('a:first').text(getPrefix(user.modes) + user.nick);
+            sortUsers();
+        }
+
+        return this;
+    };
+};
+UserList.prototype.width = 100;     // 0 to disable
+UserList.prototype.setWidth = function (newWidth) {
+    var w, u;
+    if (typeof newWidth === 'number') {
+        this.width = newWidth;
+    }
+
+    w = $('#windows');
+    u = $('#kiwi .userlist');
+
+    u.width(this.width);
+
+    return this;
+};
+
+UserList.prototype.clickHandler = function () {
+    var li = $(this).parent();
+
+    // Remove any existing userboxes
+    $('#kiwi .userbox').remove();
+
+    if ($(li).data('userbox') === this) {
+        $(li).removeData('userbox');
+    } else {
+        $('#tmpl_user_box').tmpl({nick: kiwi.front.nickStripPrefix($(this).text())}).appendTo(li);
+
+        $('#kiwi .userbox .userbox_query').click(function (ev) {
+            var nick = $('#kiwi .userbox_nick').val();
+            kiwi.front.run('/query ' + nick);
+        });
+
+        $('#kiwi .userbox .userbox_whois').click(function (ev) {
+            var nick = $('#kiwi .userbox_nick').val();
+            kiwi.front.run('/whois ' + nick);
+        });
+        $(li).data('userbox', this);
+    }
+};
 
 
 
@@ -1340,21 +1636,26 @@ kiwi.front = {
 var Utilityview = function (name) {
     var rand_name = randomString(15),
         tmp_divname = 'kiwi_window_' + rand_name,
-        tmp_userlistname = 'kiwi_userlist_' + rand_name,
         tmp_tabname = 'kiwi_tab_' + rand_name;
-    
+
     this.name = rand_name;
     this.title = name;
     this.topic = ' ';
+    this.panel = $('#panel1');
 
-    $('#kiwi .windows .scroller').append('<div id="' + tmp_divname + '" class="messages"></div>');
+    if (typeof $('.scroller', this.panel)[0] === 'undefined') {
+        this.panel.append('<div id="' + tmp_divname + '" class="messages"></div>');
+    } else {
+        $('.scroller', this.panel).append('<div id="' + tmp_divname + '" class="messages"></div>');
+    }
 
     this.tab = $('<li id="' + tmp_tabname + '">' + this.title + '</li>');
     this.tab.click(function () {
         kiwi.front.utilityviews[rand_name.toLowerCase()].show();
     });
     $('#kiwi .utilityviewlist ul').append(this.tab);
-    
+    kiwi.front.doLayoutSize();
+
     this.div = $('#' + tmp_divname);
     this.div.css('overflow', 'hidden');
 
@@ -1366,12 +1667,13 @@ Utilityview.prototype.title = null;
 Utilityview.prototype.div = null;
 Utilityview.prototype.tab = null;
 Utilityview.prototype.topic = ' ';
+Utilityview.prototype.panel = null;
 Utilityview.prototype.show = function () {
-    $('#kiwi .messages').removeClass("active");
+    $('.messages', this.panel).removeClass("active");
     $('#kiwi .userlist ul').removeClass("active");
     $('#kiwi .toolbars ul li').removeClass("active");
 
-    $('#windows').css('overflow-y', 'hidden');
+    this.panel.css('overflow-y', 'hidden');
     $('#windows').css('right', 0);
     // Activate this tab!
     this.div.addClass('active');
@@ -1388,11 +1690,18 @@ Utilityview.prototype.show = function () {
     }
 };
 
+Utilityview.prototype.setPanel = function (new_panel) {
+    this.div.detach();
+    this.panel = new_panel;
+    this.panel.append(this.div);
+    this.show();
+};
+
 Utilityview.prototype.close = function () {
     this.div.remove();
     this.tab.remove();
-    
-    if (kiwi.front.cur_channel === this) {
+
+    if (Tabview.getCurrentTab() === this) {
         kiwi.front.tabviews.server.show();
     }
     delete kiwi.front.utilityviews[this.name.toLowerCase()];
@@ -1400,7 +1709,7 @@ Utilityview.prototype.close = function () {
 
 Utilityview.prototype.addPartImage = function () {
     this.clearPartImage();
-    
+
     // We can't close this tab, so don't have the close image
     if (this.name === 'server') {
         return;
@@ -1408,10 +1717,10 @@ Utilityview.prototype.addPartImage = function () {
 
     var del_html = '<img src="/img/redcross.png" class="tab_part" />';
     this.tab.append(del_html);
-    
+
     $('.tab_part', this.tab).click(function () {
-        if (kiwi.front.cur_channel.name !== 'server') {
-            kiwi.front.cur_channel.close();
+        if (Tabview.getCurrentTab().name !== 'server') {
+            Tabview.getCurrentTab().close();
         }
     });
 };
@@ -1431,49 +1740,101 @@ Utilityview.prototype.clearPartImage = function () {
  */
 
 
-var Tabview = function () {};
+var Tabview = function (v_name) {
+    /*global Tabview, UserList */
+    var re, htmlsafe_name, tmp_divname, tmp_userlistname, tmp_tabname, userlist_enabled = true;
+
+    if (v_name.charAt(0) === kiwi.gateway.channel_prefix) {
+    //if (v_name[0] === kiwi.gateway.channel_prefix) {
+        re = new RegExp(kiwi.gateway.channel_prefix, "g");
+        htmlsafe_name = v_name.replace(re, 'pre');
+        htmlsafe_name = "chan_" + htmlsafe_name;
+    } else {
+        htmlsafe_name = 'query_' + v_name;
+        userlist_enabled = false;
+    }
+
+    tmp_divname = 'kiwi_window_' + htmlsafe_name;
+    tmp_userlistname = 'kiwi_userlist_' + htmlsafe_name;
+    tmp_tabname = 'kiwi_tab_' + htmlsafe_name;
+
+    if (!Tabview.tabExists(v_name)) {
+        $('#kiwi .windows .scroller').append('<div id="' + tmp_divname + '" class="messages"></div>');
+        //$('#kiwi .userlist').append('<ul id="' + tmp_userlistname + '"></ul>');
+        $('#kiwi .windowlist ul').append('<li id="' + tmp_tabname + '">' + v_name + '</li>');
+        $('li', $('#kiwi .windowlist ul')[0]).last().bind('click', function () {
+            var tab = Tabview.getTab(v_name);
+            if (tab) {
+                tab.show();
+            }
+        });
+    }
+    //$('#kiwi .windowlist ul .window_'+v_name).click(function(){ kiwi.front.windowShow(v_name); });
+    //kiwi.front.windowShow(v_name);
+
+    kiwi.front.tabviews[v_name.toLowerCase()] = this;
+    this.name = v_name;
+    this.div = $('#' + tmp_divname);
+    this.userlist = new UserList(htmlsafe_name);
+    this.tab = $('#' + tmp_tabname);
+    this.panel = $('#panel1');
+
+    if (!userlist_enabled) {
+        this.userlist.setWidth(0);
+    }
+    this.show();
+
+    if (typeof registerTouches === "function") {
+        //alert("Registering touch interface");
+        //registerTouches($('#'+tmp_divname));
+        registerTouches(document.getElementById(tmp_divname));
+    }
+
+    kiwi.front.doLayoutSize();
+};
 Tabview.prototype.name = null;
 Tabview.prototype.div = null;
 Tabview.prototype.userlist = null;
-Tabview.prototype.userlist_width = 100;     // 0 for disabled
 Tabview.prototype.tab = null;
 Tabview.prototype.topic = "";
 Tabview.prototype.safe_to_close = false;                // If we have been kicked/banned/etc from this channel, don't wait for a part message
+Tabview.prototype.panel = null;
 
 Tabview.prototype.show = function () {
     var w, u;
 
-    $('#kiwi .messages').removeClass("active");
+    $('.messages', this.panel).removeClass("active");
     $('#kiwi .userlist ul').removeClass("active");
     $('#kiwi .toolbars ul li').removeClass("active");
-    
+
     w = $('#windows');
     u = $('#kiwi .userlist');
 
-    w.css('overflow-y', 'scroll');
+    this.panel.css('overflow-y', 'scroll');
 
     // Set the window size accordingly
-    this.setUserlistWidth();
-
-    // Activate this tab!
-    this.div.addClass('active');
-    if (this.userlist_width > 0) {
-        this.userlist.addClass('active');
+    if (this.userlist.width > 0) {
+        this.userlist.setWidth();
+        w.css('right', u.outerWidth(true));
+        this.userlist.active(true);
         // Enable the userlist resizer
         $('#nicklist_resize').css('display', 'block');
     } else {
+        w.css('right', 0);
         // Disable the userlist resizer
         $('#nicklist_resize').css('display', 'none');
     }
+
+    this.div.addClass('active');
     this.tab.addClass('active');
-    
+
     // Add the part image to the tab
     this.addPartImage();
-    
+
     this.clearHighlight();
     kiwi.front.setTopicText(this.topic);
     kiwi.front.cur_channel = this;
-    
+
     // If we're using fancy scrolling, refresh it
     if (touch_scroll) {
         touch_scroll.refresh();
@@ -1488,35 +1849,18 @@ Tabview.prototype.show = function () {
 Tabview.prototype.close = function () {
     this.div.remove();
     this.userlist.remove();
+    this.userlist = null;
     this.tab.remove();
-    
+
     if (kiwi.front.cur_channel === this) {
         kiwi.front.tabviews.server.show();
     }
     delete kiwi.front.tabviews[this.name.toLowerCase()];
 };
 
-Tabview.prototype.setUserlistWidth = function (new_width) {
-    var w, u;
-    if (typeof new_width === 'number') {
-        this.userlist_width = new_width;
-    }
-
-    w = $('#windows');
-    u = $('#kiwi .userlist');
-
-    // Set the window size accordingly
-    if (this.userlist_width > 0) {
-        u.width(this.userlist_width);
-        w.css('right', u.outerWidth(true));
-    } else {
-        w.css('right', 0);
-    }    
-};
-
 Tabview.prototype.addPartImage = function () {
     this.clearPartImage();
-    
+
     // We can't close this tab, so don't have the close image
     if (this.name === 'server') {
         return;
@@ -1524,7 +1868,7 @@ Tabview.prototype.addPartImage = function () {
 
     var del_html = '<img src="/img/redcross.png" class="tab_part" />';
     this.tab.append(del_html);
-    
+
     $('.tab_part', this.tab).click(function () {
         if (kiwi.front.isChannel($(this).parent().text())) {
             kiwi.front.run("/part");
@@ -1542,16 +1886,16 @@ Tabview.prototype.clearPartImage = function () {
 };
 
 Tabview.prototype.addMsg = function (time, nick, msg, type, style) {
-    var self, tmp, plugin_ret, i, d, re, line_msg, next;
-    
+    var self, tmp, d, re, line_msg;
+
     self = this;
-    
+
     tmp = {msg: msg, time: time, nick: nick, tabview: this.name};
     tmp = kiwi.plugs.run('addmsg', tmp);
     if (!tmp) {
         return;
     }
-    
+
 
     msg = tmp.msg;
     time = tmp.time;
@@ -1561,25 +1905,25 @@ Tabview.prototype.addMsg = function (time, nick, msg, type, style) {
         d = new Date();
         time = d.getHours().toString().lpad(2, "0") + ":" + d.getMinutes().toString().lpad(2, "0") + ":" + d.getSeconds().toString().lpad(2, "0");
     }
-    
+
     // The CSS class (action, topic, notice, etc)
     if (typeof type !== "string") {
         type = '';
     }
-    
+
     // Make sure we don't have NaN or something
     if (typeof msg !== "string") {
         msg = '';
     }
-    
-    msg = kiwi.front.format(msg);
-    
+
     // Make the channels clickable
     re = new RegExp('\\B(' + kiwi.gateway.channel_prefix + '[^ ,.\\007]+)', 'g');
     msg = msg.replace(re, function (match) {
         return '<a class="chan">' + match + '</a>';
     });
 
+    msg = kiwi.front.formatIRCMsg(msg);
+
     // Build up and add the line
     line_msg = $('<div class="msg ' + type + '"><div class="time">' + time + '</div><div class="nick">' + nick + '</div><div class="text" style="' + style + '">' + msg + ' </div></div>');
     this.div.append(line_msg);
@@ -1597,59 +1941,17 @@ Tabview.prototype.addMsg = function (time, nick, msg, type, style) {
 };
 
 Tabview.prototype.scrollBottom = function () {
-    var w = $('#windows');
-    w[0].scrollTop = w[0].scrollHeight;
+    var panel = this.panel;
+    panel[0].scrollTop = panel[0].scrollHeight;
 };
 
 Tabview.prototype.changeNick = function (newNick, oldNick) {
-    this.userlist.children().each(function () {
-        var item = $('a.nick', this);
-        if (kiwi.front.nickStripPrefix(item.text()) === oldNick) {
-            item.text(kiwi.front.nickGetPrefix(item.text()) + newNick);
-            document.temp_chan = 1;
-        }
-    });
-    
-    if (typeof document.temp_chan !== "undefined") {
+    var inChan = this.userlist.hasUser(oldNick);
+    if (inChan) {
+        this.userlist.renameUser(oldNick, newNick);
         this.addMsg(null, ' ', '=== ' + oldNick + ' is now known as ' + newNick, 'action changenick');
-        delete document.temp_chan;
-        this.userlistSort();
     }
 };
-
-Tabview.prototype.userlistSort = function () {
-    var ul = this.userlist,
-        listitems = ul.children('li').get(),
-        prefix;
-    listitems.sort(function (a, b) {
-        var compA = $(a).text().toUpperCase(),
-            compB = $(b).text().toUpperCase(),
-            i;
-        
-        // Sort by prefixes first
-        for (i in kiwi.gateway.user_prefixes) {
-            prefix = kiwi.gateway.user_prefixes[i].symbol;
-            
-            if (compA.charAt(0) === prefix && compB.charAt(0) === prefix) {
-                // Both have the same prefix, string compare time
-                return 0;
-            }
-            
-            if (compA.charAt(0) === prefix && compB.charAt(0) !== prefix) {
-                return -1;
-            }
-            
-            if (compA.charAt(0) !== prefix && compB.charAt(0) === prefix) {
-                return 1;
-            }
-        }
-               
-        // No prefixes, compare by string
-        return (compA < compB) ? -1 : (compA > compB) ? 1 : 0;
-    });
-    $.each(listitems, function (idx, itm) { ul.append(itm); });
-};
-
 Tabview.prototype.highlight = function () {
     this.tab.addClass('highlight');
 };
@@ -1667,10 +1969,28 @@ Tabview.prototype.changeTopic = function (new_topic) {
         kiwi.front.setTopicText(new_topic);
     }
 };
-
-
-
-
+// Static functions
+Tabview.tabExists = function (name) {
+    var ret = (typeof kiwi.front.tabviews[name.toLowerCase()] !== 'undefined');
+    return ret;
+};
+Tabview.getTab = function (name) {
+    if (Tabview.tabExists(name)) {
+        var ret = kiwi.front.tabviews[name.toLowerCase()];
+        return ret;
+    } else {
+        return null;
+    }
+};
+Tabview.getServerTab = function () {
+    return kiwi.front.tabviews.server;
+};
+Tabview.getAllTabs = function () {
+    return kiwi.front.tabviews;
+};
+Tabview.getCurrentTab = function () {
+    return kiwi.front.cur_channel;
+};
 
 var Box = function (classname) {
     this.id = randomString(10);
@@ -1678,13 +1998,13 @@ var Box = function (classname) {
     $('#kiwi').append(tmp);
     this.box = $('#' + this.id);
     this.content = $('#' + this.id + ' .boxarea');
-    
+
     this.box.draggable({ stack: ".box" });
     this.content.click(function () {});
     //this.box.click(function(){ $(this)..css });
 };
 Box.prototype.create = function (name, classname) {
-    
+
 };
 Box.prototype.id = null;
 Box.prototype.box = null;
@@ -1698,6 +2018,6 @@ Box.prototype.destroy = function () {
         }
     }
 };
-Box.prototype.height = function () { 
+Box.prototype.height = function () {
     return this.box.height();
 };