Merge branch 'master' of github.com:prawnsalad/KiwiIRC
authorJack Allnutt <m2ys4u@gmail.com>
Mon, 27 May 2013 12:25:57 +0000 (13:25 +0100)
committerJack Allnutt <m2ys4u@gmail.com>
Mon, 27 May 2013 12:25:57 +0000 (13:25 +0100)
30 files changed:
client/assets/backbone.min.js
client/assets/css/style.css
client/assets/dev/app.js
client/assets/dev/build.js
client/assets/dev/index.html.tmpl
client/assets/dev/model_applet.js
client/assets/dev/model_application.js
client/assets/dev/model_gateway.js
client/assets/dev/model_network.js [new file with mode: 0644]
client/assets/dev/model_networkpanellist.js [new file with mode: 0644]
client/assets/dev/model_newconnection.js [new file with mode: 0644]
client/assets/dev/model_panel.js
client/assets/dev/model_panellist.js
client/assets/dev/model_server.js
client/assets/dev/utils.js
client/assets/dev/view.js
client/assets/lodash.min.js
client/assets/plugins/filepicker.html
kiwi.bat [new file with mode: 0755]
package.json
server/irc/channel.js
server/irc/commands.js
server/irc/connection.js
server/irc/eventbinder.js
server/irc/server.js
server/irc/user.js
server/kiwi.js
server/socks.js [deleted file]
server/weblistener.js
server_modules/example.js

index 5cba874b0e532e41977f7bbccda84b4cca20f582..4aa5e8356cf5c85541893d19f984bb773909bdb5 100644 (file)
@@ -1,38 +1,4 @@
-// Backbone.js 0.9.2\r
-\r
-// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.\r
-// Backbone may be freely distributed under the MIT license.\r
-// For all details and documentation:\r
-// http://backbonejs.org\r
-(function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks=\r
-{});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g=\r
-z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent=\r
-{};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null==\r
-b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent:\r
-b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)};\r
-a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error,\r
-h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t();\r
-return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending=\r
-{};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length||\r
-!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator);\r
-this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c<d;c++){if(!(e=a[c]=this._prepareModel(a[c],b)))throw Error("Can't add an invalid model to a collection");g=e.cid;i=e.id;j[g]||this._byCid[g]||null!=i&&(k[i]||this._byId[i])?\r
-l.push(c):j[g]=k[i]=e}for(c=l.length;c--;)a.splice(l[c],1);c=0;for(d=a.length;c<d;c++)(e=a[c]).on("all",this._onModelEvent,this),this._byCid[e.cid]=e,null!=e.id&&(this._byId[e.id]=e);this.length+=d;A.apply(this.models,[null!=b.at?b.at:this.models.length,0].concat(a));this.comparator&&this.sort({silent:!0});if(b.silent)return this;c=0;for(d=this.models.length;c<d;c++)if(j[(e=this.models[c]).cid])b.index=c,e.trigger("add",e,this,b);return this},remove:function(a,b){var c,d,e,g;b||(b={});a=f.isArray(a)?\r
-a.slice():[a];c=0;for(d=a.length;c<d;c++)if(g=this.getByCid(a[c])||this.get(a[c]))delete this._byId[g.id],delete this._byCid[g.cid],e=this.indexOf(g),this.models.splice(e,1),this.length--,b.silent||(b.index=e,g.trigger("remove",g,this,b)),this._removeReference(g);return this},push:function(a,b){a=this._prepareModel(a,b);this.add(a,b);return a},pop:function(a){var b=this.at(this.length-1);this.remove(b,a);return b},unshift:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:0},b));return a},\r
-shift:function(a){var b=this.at(0);this.remove(b,a);return b},get:function(a){return null==a?void 0:this._byId[null!=a.id?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},where:function(a){return f.isEmpty(a)?[]:this.filter(function(b){for(var c in a)if(a[c]!==b.get(c))return!1;return!0})},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");var b=f.bind(this.comparator,this);1==this.comparator.length?\r
-this.models=this.sortBy(b):this.models.sort(b);a.silent||this.trigger("reset",this,a);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},reset:function(a,b){a||(a=[]);b||(b={});for(var c=0,d=this.models.length;c<d;c++)this._removeReference(this.models[c]);this._reset();this.add(a,f.extend({silent:!0},b));b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a=a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=this,c=a.success;a.success=function(d,\r
-e,f){b[a.add?"add":"reset"](b.parse(d,f),a);c&&c(b,d)};a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},create:function(a,b){var c=this,b=b?f.clone(b):{},a=this._prepareModel(a,b);if(!a)return!1;b.wait||c.add(a,b);var d=b.success;b.success=function(e,f){b.wait&&c.add(e,b);d?d(e,f):e.trigger("sync",a,f,b)};a.save(null,b);return a},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId=\r
-{};this._byCid={}},_prepareModel:function(a,b){b||(b={});a instanceof o?a.collection||(a.collection=this):(b.collection=this,a=new this.model(a,b),a._validate(a.attributes,b)||(a=!1));return a},_removeReference:function(a){this==a.collection&&delete a.collection;a.off("all",this._onModelEvent,this)},_onModelEvent:function(a,b,c,d){("add"==a||"remove"==a)&&c!=this||("destroy"==a&&this.remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],this._byId[b.id]=b),this.trigger.apply(this,\r
-arguments))}});f.each("forEach,each,map,reduce,reduceRight,find,detect,filter,select,reject,every,all,some,any,include,contains,invoke,max,min,sortBy,sortedIndex,toArray,size,first,initial,rest,last,without,indexOf,shuffle,lastIndexOf,isEmpty,groupBy".split(","),function(a){r.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});var u=g.Router=function(a){a||(a={});a.routes&&(this.routes=a.routes);this._bindRoutes();this.initialize.apply(this,arguments)},B=/:\w+/g,\r
-C=/\*\w+/g,D=/[-[\]{}()+?.,\\^$|#\s]/g;f.extend(u.prototype,k,{initialize:function(){},route:function(a,b,c){g.history||(g.history=new m);f.isRegExp(a)||(a=this._routeToRegExp(a));c||(c=this[b]);g.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c&&c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d));g.history.trigger("route",this,b,d)},this));return this},navigate:function(a,b){g.history.navigate(a,b)},_bindRoutes:function(){if(this.routes){var a=[],b;for(b in this.routes)a.unshift([b,\r
-this.routes[b]]);b=0;for(var c=a.length;b<c;b++)this.route(a[b][0],a[b][1],this[a[b][1]])}},_routeToRegExp:function(a){a=a.replace(D,"\\$&").replace(B,"([^/]+)").replace(C,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});var m=g.History=function(){this.handlers=[];f.bindAll(this,"checkUrl")},s=/^[#\/]/,E=/msie [\w.]+/;m.started=!1;f.extend(m.prototype,k,{interval:50,getHash:function(a){return(a=(a?a.location:window.location).href.match(/#(.*)$/))?a[1]:\r
-""},getFragment:function(a,b){if(null==a)if(this._hasPushState||b){var a=window.location.pathname,c=window.location.search;c&&(a+=c)}else a=this.getHash();a.indexOf(this.options.root)||(a=a.substr(this.options.root.length));return a.replace(s,"")},start:function(a){if(m.started)throw Error("Backbone.history has already been started");m.started=!0;this.options=f.extend({},{root:"/"},this.options,a);this._wantsHashChange=!1!==this.options.hashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=\r
-!(!this.options.pushState||!window.history||!window.history.pushState);var a=this.getFragment(),b=document.documentMode;if(b=E.exec(navigator.userAgent.toLowerCase())&&(!b||7>=b))this.iframe=i('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a);this._hasPushState?i(window).bind("popstate",this.checkUrl):this._wantsHashChange&&"onhashchange"in window&&!b?i(window).bind("hashchange",this.checkUrl):this._wantsHashChange&&(this._checkUrlInterval=setInterval(this.checkUrl,\r
-this.interval));this.fragment=a;a=window.location;b=a.pathname==this.options.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment),!0;this._wantsPushState&&this._hasPushState&&b&&a.hash&&(this.fragment=this.getHash().replace(s,""),window.history.replaceState({},document.title,a.protocol+"//"+a.host+this.options.root+this.fragment));if(!this.options.silent)return this.loadUrl()},\r
-stop:function(){i(window).unbind("popstate",this.checkUrl).unbind("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);m.started=!1},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.getHash(this.iframe)));if(a==this.fragment)return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(this.getHash())},loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,\r
-function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){if(!m.started)return!1;if(!b||!0===b)b={trigger:b};var c=(a||"").replace(s,"");this.fragment!=c&&(this._hasPushState?(0!=c.indexOf(this.options.root)&&(c=this.options.root+c),this.fragment=c,window.history[b.replace?"replaceState":"pushState"]({},document.title,c)):this._wantsHashChange?(this.fragment=c,this._updateHash(window.location,c,b.replace),this.iframe&&c!=this.getFragment(this.getHash(this.iframe))&&(b.replace||\r
-this.iframe.document.open().close(),this._updateHash(this.iframe.location,c,b.replace))):window.location.assign(this.options.root+a),b.trigger&&this.loadUrl(a))},_updateHash:function(a,b,c){c?a.replace(a.toString().replace(/(javascript:|#).*$/,"")+"#"+b):a.hash=b}});var v=g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()},F=/^(\S+)\s*(.*)$/,w="model,collection,el,id,attributes,className,tagName".split(",");\r
-f.extend(v.prototype,k,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();return this},make:function(a,b,c){a=document.createElement(a);b&&i(a).attr(b);c&&i(a).html(c);return a},setElement:function(a,b){this.$el&&this.undelegateEvents();this.$el=a instanceof i?a:i(a);this.el=this.$el[0];!1!==b&&this.delegateEvents();return this},delegateEvents:function(a){if(a||(a=n(this,"events"))){this.undelegateEvents();\r
-for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);if(!c)throw Error('Method "'+a[b]+'" does not exist');var d=b.match(F),e=d[1],d=d[2],c=f.bind(c,this),e=e+(".delegateEvents"+this.cid);""===d?this.$el.bind(e,c):this.$el.delegate(d,e,c)}}},undelegateEvents:function(){this.$el.unbind(".delegateEvents"+this.cid)},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b=0,c=w.length;b<c;b++){var d=w[b];a[d]&&(this[d]=a[d])}this.options=a},_ensureElement:function(){if(this.el)this.setElement(this.el,\r
-!1);else{var a=n(this,"attributes")||{};this.id&&(a.id=this.id);this.className&&(a["class"]=this.className);this.setElement(this.make(this.tagName,a),!1)}}});o.extend=r.extend=u.extend=v.extend=function(a,b){var c=G(this,a,b);c.extend=this.extend;return c};var H={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};g.sync=function(a,b,c){var d=H[a];c||(c={});var e={type:d,dataType:"json"};c.url||(e.url=n(b,"url")||t());if(!c.data&&b&&("create"==a||"update"==a))e.contentType="application/json",\r
-e.data=JSON.stringify(b.toJSON());g.emulateJSON&&(e.contentType="application/x-www-form-urlencoded",e.data=e.data?{model:e.data}:{});if(g.emulateHTTP&&("PUT"===d||"DELETE"===d))g.emulateJSON&&(e.data._method=d),e.type="POST",e.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d)};"GET"!==e.type&&!g.emulateJSON&&(e.processData=!1);return i.ajax(f.extend(e,c))};g.wrapError=function(a,b,c){return function(d,e){e=d===b?e:d;a?a(b,e,c):b.trigger("error",b,e,c)}};var x=function(){},G=function(a,\r
-b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){a.apply(this,arguments)};f.extend(d,a);x.prototype=a.prototype;d.prototype=new x;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},n=function(a,b){return!a||!a[b]?null:f.isFunction(a[b])?a[b]():a[b]},t=function(){throw Error('A "url" property or function must be specified');}}).call(this);
\ No newline at end of file
+(function(){var root=this;var previousBackbone=root.Backbone;var array=[];var push=array.push;var slice=array.slice;var splice=array.splice;var Backbone;if(typeof exports!=="undefined"){Backbone=exports}else{Backbone=root.Backbone={}}Backbone.VERSION="1.0.0";var _=root._;if(!_&&typeof require!=="undefined")_=require("underscore");Backbone.$=root.jQuery||root.Zepto||root.ender||root.$;Backbone.noConflict=function(){root.Backbone=previousBackbone;return this};Backbone.emulateHTTP=false;Backbone.emulateJSON=false;var Events=Backbone.Events={on:function(name,callback,context){if(!eventsApi(this,"on",name,[callback,context])||!callback)return this;this._events||(this._events={});var events=this._events[name]||(this._events[name]=[]);events.push({callback:callback,context:context,ctx:context||this});return this},once:function(name,callback,context){if(!eventsApi(this,"once",name,[callback,context])||!callback)return this;var self=this;var once=_.once(function(){self.off(name,once);callback.apply(this,arguments)});once._callback=callback;return this.on(name,once,context)},off:function(name,callback,context){var retain,ev,events,names,i,l,j,k;if(!this._events||!eventsApi(this,"off",name,[callback,context]))return this;if(!name&&!callback&&!context){this._events={};return this}names=name?[name]:_.keys(this._events);for(i=0,l=names.length;i<l;i++){name=names[i];if(events=this._events[name]){this._events[name]=retain=[];if(callback||context){for(j=0,k=events.length;j<k;j++){ev=events[j];if(callback&&callback!==ev.callback&&callback!==ev.callback._callback||context&&context!==ev.context){retain.push(ev)}}}if(!retain.length)delete this._events[name]}}return this},trigger:function(name){if(!this._events)return this;var args=slice.call(arguments,1);if(!eventsApi(this,"trigger",name,args))return this;var events=this._events[name];var allEvents=this._events.all;if(events)triggerEvents(events,args);if(allEvents)triggerEvents(allEvents,arguments);return this},stopListening:function(obj,name,callback){var listeners=this._listeners;if(!listeners)return this;var deleteListener=!name&&!callback;if(typeof name==="object")callback=this;if(obj)(listeners={})[obj._listenerId]=obj;for(var id in listeners){listeners[id].off(name,callback,this);if(deleteListener)delete this._listeners[id]}return this}};var eventSplitter=/\s+/;var eventsApi=function(obj,action,name,rest){if(!name)return true;if(typeof name==="object"){for(var key in name){obj[action].apply(obj,[key,name[key]].concat(rest))}return false}if(eventSplitter.test(name)){var names=name.split(eventSplitter);for(var i=0,l=names.length;i<l;i++){obj[action].apply(obj,[names[i]].concat(rest))}return false}return true};var triggerEvents=function(events,args){var ev,i=-1,l=events.length,a1=args[0],a2=args[1],a3=args[2];switch(args.length){case 0:while(++i<l)(ev=events[i]).callback.call(ev.ctx);return;case 1:while(++i<l)(ev=events[i]).callback.call(ev.ctx,a1);return;case 2:while(++i<l)(ev=events[i]).callback.call(ev.ctx,a1,a2);return;case 3:while(++i<l)(ev=events[i]).callback.call(ev.ctx,a1,a2,a3);return;default:while(++i<l)(ev=events[i]).callback.apply(ev.ctx,args)}};var listenMethods={listenTo:"on",listenToOnce:"once"};_.each(listenMethods,function(implementation,method){Events[method]=function(obj,name,callback){var listeners=this._listeners||(this._listeners={});var id=obj._listenerId||(obj._listenerId=_.uniqueId("l"));listeners[id]=obj;if(typeof name==="object")callback=this;obj[implementation](name,callback,this);return this}});Events.bind=Events.on;Events.unbind=Events.off;_.extend(Backbone,Events);var Model=Backbone.Model=function(attributes,options){var defaults;var attrs=attributes||{};options||(options={});this.cid=_.uniqueId("c");this.attributes={};_.extend(this,_.pick(options,modelOptions));if(options.parse)attrs=this.parse(attrs,options)||{};if(defaults=_.result(this,"defaults")){attrs=_.defaults({},attrs,defaults)}this.set(attrs,options);this.changed={};this.initialize.apply(this,arguments)};var modelOptions=["url","urlRoot","collection"];_.extend(Model.prototype,Events,{changed:null,validationError:null,idAttribute:"id",initialize:function(){},toJSON:function(options){return _.clone(this.attributes)},sync:function(){return Backbone.sync.apply(this,arguments)},get:function(attr){return this.attributes[attr]},escape:function(attr){return _.escape(this.get(attr))},has:function(attr){return this.get(attr)!=null},set:function(key,val,options){var attr,attrs,unset,changes,silent,changing,prev,current;if(key==null)return this;if(typeof key==="object"){attrs=key;options=val}else{(attrs={})[key]=val}options||(options={});if(!this._validate(attrs,options))return false;unset=options.unset;silent=options.silent;changes=[];changing=this._changing;this._changing=true;if(!changing){this._previousAttributes=_.clone(this.attributes);this.changed={}}current=this.attributes,prev=this._previousAttributes;if(this.idAttribute in attrs)this.id=attrs[this.idAttribute];for(attr in attrs){val=attrs[attr];if(!_.isEqual(current[attr],val))changes.push(attr);if(!_.isEqual(prev[attr],val)){this.changed[attr]=val}else{delete this.changed[attr]}unset?delete current[attr]:current[attr]=val}if(!silent){if(changes.length)this._pending=true;for(var i=0,l=changes.length;i<l;i++){this.trigger("change:"+changes[i],this,current[changes[i]],options)}}if(changing)return this;if(!silent){while(this._pending){this._pending=false;this.trigger("change",this,options)}}this._pending=false;this._changing=false;return this},unset:function(attr,options){return this.set(attr,void 0,_.extend({},options,{unset:true}))},clear:function(options){var attrs={};for(var key in this.attributes)attrs[key]=void 0;return this.set(attrs,_.extend({},options,{unset:true}))},hasChanged:function(attr){if(attr==null)return!_.isEmpty(this.changed);return _.has(this.changed,attr)},changedAttributes:function(diff){if(!diff)return this.hasChanged()?_.clone(this.changed):false;var val,changed=false;var old=this._changing?this._previousAttributes:this.attributes;for(var attr in diff){if(_.isEqual(old[attr],val=diff[attr]))continue;(changed||(changed={}))[attr]=val}return changed},previous:function(attr){if(attr==null||!this._previousAttributes)return null;return this._previousAttributes[attr]},previousAttributes:function(){return _.clone(this._previousAttributes)},fetch:function(options){options=options?_.clone(options):{};if(options.parse===void 0)options.parse=true;var model=this;var success=options.success;options.success=function(resp){if(!model.set(model.parse(resp,options),options))return false;if(success)success(model,resp,options);model.trigger("sync",model,resp,options)};wrapError(this,options);return this.sync("read",this,options)},save:function(key,val,options){var attrs,method,xhr,attributes=this.attributes;if(key==null||typeof key==="object"){attrs=key;options=val}else{(attrs={})[key]=val}if(attrs&&(!options||!options.wait)&&!this.set(attrs,options))return false;options=_.extend({validate:true},options);if(!this._validate(attrs,options))return false;if(attrs&&options.wait){this.attributes=_.extend({},attributes,attrs)}if(options.parse===void 0)options.parse=true;var model=this;var success=options.success;options.success=function(resp){model.attributes=attributes;var serverAttrs=model.parse(resp,options);if(options.wait)serverAttrs=_.extend(attrs||{},serverAttrs);if(_.isObject(serverAttrs)&&!model.set(serverAttrs,options)){return false}if(success)success(model,resp,options);model.trigger("sync",model,resp,options)};wrapError(this,options);method=this.isNew()?"create":options.patch?"patch":"update";if(method==="patch")options.attrs=attrs;xhr=this.sync(method,this,options);if(attrs&&options.wait)this.attributes=attributes;return xhr},destroy:function(options){options=options?_.clone(options):{};var model=this;var success=options.success;var destroy=function(){model.trigger("destroy",model,model.collection,options)};options.success=function(resp){if(options.wait||model.isNew())destroy();if(success)success(model,resp,options);if(!model.isNew())model.trigger("sync",model,resp,options)};if(this.isNew()){options.success();return false}wrapError(this,options);var xhr=this.sync("delete",this,options);if(!options.wait)destroy();return xhr},url:function(){var base=_.result(this,"urlRoot")||_.result(this.collection,"url")||urlError();if(this.isNew())return base;return base+(base.charAt(base.length-1)==="/"?"":"/")+encodeURIComponent(this.id)},parse:function(resp,options){return resp},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return this.id==null},isValid:function(options){return this._validate({},_.extend(options||{},{validate:true}))},_validate:function(attrs,options){if(!options.validate||!this.validate)return true;attrs=_.extend({},this.attributes,attrs);var error=this.validationError=this.validate(attrs,options)||null;if(!error)return true;this.trigger("invalid",this,error,_.extend(options||{},{validationError:error}));return false}});var modelMethods=["keys","values","pairs","invert","pick","omit"];_.each(modelMethods,function(method){Model.prototype[method]=function(){var args=slice.call(arguments);args.unshift(this.attributes);return _[method].apply(_,args)}});var Collection=Backbone.Collection=function(models,options){options||(options={});if(options.url)this.url=options.url;if(options.model)this.model=options.model;if(options.comparator!==void 0)this.comparator=options.comparator;this._reset();this.initialize.apply(this,arguments);if(models)this.reset(models,_.extend({silent:true},options))};var setOptions={add:true,remove:true,merge:true};var addOptions={add:true,merge:false,remove:false};_.extend(Collection.prototype,Events,{model:Model,initialize:function(){},toJSON:function(options){return this.map(function(model){return model.toJSON(options)})},sync:function(){return Backbone.sync.apply(this,arguments)},add:function(models,options){return this.set(models,_.defaults(options||{},addOptions))},remove:function(models,options){models=_.isArray(models)?models.slice():[models];options||(options={});var i,l,index,model;for(i=0,l=models.length;i<l;i++){model=this.get(models[i]);if(!model)continue;delete this._byId[model.id];delete this._byId[model.cid];index=this.indexOf(model);this.models.splice(index,1);this.length--;if(!options.silent){options.index=index;model.trigger("remove",model,this,options)}this._removeReference(model)}return this},set:function(models,options){options=_.defaults(options||{},setOptions);if(options.parse)models=this.parse(models,options);if(!_.isArray(models))models=models?[models]:[];var i,l,model,attrs,existing,sort;var at=options.at;var sortable=this.comparator&&at==null&&options.sort!==false;var sortAttr=_.isString(this.comparator)?this.comparator:null;var toAdd=[],toRemove=[],modelMap={};for(i=0,l=models.length;i<l;i++){if(!(model=this._prepareModel(models[i],options)))continue;if(existing=this.get(model)){if(options.remove)modelMap[existing.cid]=true;if(options.merge){existing.set(model.attributes,options);if(sortable&&!sort&&existing.hasChanged(sortAttr))sort=true}}else if(options.add){toAdd.push(model);model.on("all",this._onModelEvent,this);this._byId[model.cid]=model;if(model.id!=null)this._byId[model.id]=model}}if(options.remove){for(i=0,l=this.length;i<l;++i){if(!modelMap[(model=this.models[i]).cid])toRemove.push(model)}if(toRemove.length)this.remove(toRemove,options)}if(toAdd.length){if(sortable)sort=true;this.length+=toAdd.length;if(at!=null){splice.apply(this.models,[at,0].concat(toAdd))}else{push.apply(this.models,toAdd)}}if(sort)this.sort({silent:true});if(options.silent)return this;for(i=0,l=toAdd.length;i<l;i++){(model=toAdd[i]).trigger("add",model,this,options)}if(sort)this.trigger("sort",this,options);return this},reset:function(models,options){options||(options={});for(var i=0,l=this.models.length;i<l;i++){this._removeReference(this.models[i])}options.previousModels=this.models;this._reset();this.add(models,_.extend({silent:true},options));if(!options.silent)this.trigger("reset",this,options);return this},push:function(model,options){model=this._prepareModel(model,options);this.add(model,_.extend({at:this.length},options));return model},pop:function(options){var model=this.at(this.length-1);this.remove(model,options);return model},unshift:function(model,options){model=this._prepareModel(model,options);this.add(model,_.extend({at:0},options));return model},shift:function(options){var model=this.at(0);this.remove(model,options);return model},slice:function(begin,end){return this.models.slice(begin,end)},get:function(obj){if(obj==null)return void 0;return this._byId[obj.id!=null?obj.id:obj.cid||obj]},at:function(index){return this.models[index]},where:function(attrs,first){if(_.isEmpty(attrs))return first?void 0:[];return this[first?"find":"filter"](function(model){for(var key in attrs){if(attrs[key]!==model.get(key))return false}return true})},findWhere:function(attrs){return this.where(attrs,true)},sort:function(options){if(!this.comparator)throw new Error("Cannot sort a set without a comparator");options||(options={});if(_.isString(this.comparator)||this.comparator.length===1){this.models=this.sortBy(this.comparator,this)}else{this.models.sort(_.bind(this.comparator,this))}if(!options.silent)this.trigger("sort",this,options);return this},sortedIndex:function(model,value,context){value||(value=this.comparator);var iterator=_.isFunction(value)?value:function(model){return model.get(value)};return _.sortedIndex(this.models,model,iterator,context)},pluck:function(attr){return _.invoke(this.models,"get",attr)},fetch:function(options){options=options?_.clone(options):{};if(options.parse===void 0)options.parse=true;var success=options.success;var collection=this;options.success=function(resp){var method=options.reset?"reset":"set";collection[method](resp,options);if(success)success(collection,resp,options);collection.trigger("sync",collection,resp,options)};wrapError(this,options);return this.sync("read",this,options)},create:function(model,options){options=options?_.clone(options):{};if(!(model=this._prepareModel(model,options)))return false;if(!options.wait)this.add(model,options);var collection=this;var success=options.success;options.success=function(resp){if(options.wait)collection.add(model,options);if(success)success(model,resp,options)};model.save(null,options);return model},parse:function(resp,options){return resp},clone:function(){return new this.constructor(this.models)},_reset:function(){this.length=0;this.models=[];this._byId={}},_prepareModel:function(attrs,options){if(attrs instanceof Model){if(!attrs.collection)attrs.collection=this;return attrs}options||(options={});options.collection=this;var model=new this.model(attrs,options);if(!model._validate(attrs,options)){this.trigger("invalid",this,attrs,options);return false}return model},_removeReference:function(model){if(this===model.collection)delete model.collection;model.off("all",this._onModelEvent,this)},_onModelEvent:function(event,model,collection,options){if((event==="add"||event==="remove")&&collection!==this)return;if(event==="destroy")this.remove(model,options);if(model&&event==="change:"+model.idAttribute){delete this._byId[model.previous(model.idAttribute)];if(model.id!=null)this._byId[model.id]=model}this.trigger.apply(this,arguments)}});var methods=["forEach","each","map","collect","reduce","foldl","inject","reduceRight","foldr","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","max","min","toArray","size","first","head","take","initial","rest","tail","drop","last","without","indexOf","shuffle","lastIndexOf","isEmpty","chain"];_.each(methods,function(method){Collection.prototype[method]=function(){var args=slice.call(arguments);args.unshift(this.models);return _[method].apply(_,args)}});var attributeMethods=["groupBy","countBy","sortBy"];_.each(attributeMethods,function(method){Collection.prototype[method]=function(value,context){var iterator=_.isFunction(value)?value:function(model){return model.get(value)};return _[method](this.models,iterator,context)}});var View=Backbone.View=function(options){this.cid=_.uniqueId("view");this._configure(options||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()};var delegateEventSplitter=/^(\S+)\s*(.*)$/;var viewOptions=["model","collection","el","id","attributes","className","tagName","events"];_.extend(View.prototype,Events,{tagName:"div",$:function(selector){return this.$el.find(selector)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();this.stopListening();return this},setElement:function(element,delegate){if(this.$el)this.undelegateEvents();this.$el=element instanceof Backbone.$?element:Backbone.$(element);this.el=this.$el[0];if(delegate!==false)this.delegateEvents();return this},delegateEvents:function(events){if(!(events||(events=_.result(this,"events"))))return this;this.undelegateEvents();for(var key in events){var method=events[key];if(!_.isFunction(method))method=this[events[key]];if(!method)continue;var match=key.match(delegateEventSplitter);var eventName=match[1],selector=match[2];method=_.bind(method,this);eventName+=".delegateEvents"+this.cid;if(selector===""){this.$el.on(eventName,method)}else{this.$el.on(eventName,selector,method)}}return this},undelegateEvents:function(){this.$el.off(".delegateEvents"+this.cid);return this},_configure:function(options){if(this.options)options=_.extend({},_.result(this,"options"),options);_.extend(this,_.pick(options,viewOptions));this.options=options},_ensureElement:function(){if(!this.el){var attrs=_.extend({},_.result(this,"attributes"));if(this.id)attrs.id=_.result(this,"id");if(this.className)attrs["class"]=_.result(this,"className");var $el=Backbone.$("<"+_.result(this,"tagName")+">").attr(attrs);this.setElement($el,false)}else{this.setElement(_.result(this,"el"),false)}}});Backbone.sync=function(method,model,options){var type=methodMap[method];_.defaults(options||(options={}),{emulateHTTP:Backbone.emulateHTTP,emulateJSON:Backbone.emulateJSON});var params={type:type,dataType:"json"};if(!options.url){params.url=_.result(model,"url")||urlError()}if(options.data==null&&model&&(method==="create"||method==="update"||method==="patch")){params.contentType="application/json";params.data=JSON.stringify(options.attrs||model.toJSON(options))}if(options.emulateJSON){params.contentType="application/x-www-form-urlencoded";params.data=params.data?{model:params.data}:{}}if(options.emulateHTTP&&(type==="PUT"||type==="DELETE"||type==="PATCH")){params.type="POST";if(options.emulateJSON)params.data._method=type;var beforeSend=options.beforeSend;options.beforeSend=function(xhr){xhr.setRequestHeader("X-HTTP-Method-Override",type);if(beforeSend)return beforeSend.apply(this,arguments)}}if(params.type!=="GET"&&!options.emulateJSON){params.processData=false}if(params.type==="PATCH"&&window.ActiveXObject&&!(window.external&&window.external.msActiveXFilteringEnabled)){params.xhr=function(){return new ActiveXObject("Microsoft.XMLHTTP")}}var xhr=options.xhr=Backbone.ajax(_.extend(params,options));model.trigger("request",model,xhr,options);return xhr};var methodMap={create:"POST",update:"PUT",patch:"PATCH","delete":"DELETE",read:"GET"};Backbone.ajax=function(){return Backbone.$.ajax.apply(Backbone.$,arguments)};var Router=Backbone.Router=function(options){options||(options={});if(options.routes)this.routes=options.routes;this._bindRoutes();this.initialize.apply(this,arguments)};var optionalParam=/\((.*?)\)/g;var namedParam=/(\(\?)?:\w+/g;var splatParam=/\*\w+/g;var escapeRegExp=/[\-{}\[\]+?.,\\\^$|#\s]/g;_.extend(Router.prototype,Events,{initialize:function(){},route:function(route,name,callback){if(!_.isRegExp(route))route=this._routeToRegExp(route);if(_.isFunction(name)){callback=name;name=""}if(!callback)callback=this[name];var router=this;Backbone.history.route(route,function(fragment){var args=router._extractParameters(route,fragment);callback&&callback.apply(router,args);router.trigger.apply(router,["route:"+name].concat(args));router.trigger("route",name,args);Backbone.history.trigger("route",router,name,args)});return this},navigate:function(fragment,options){Backbone.history.navigate(fragment,options);return this},_bindRoutes:function(){if(!this.routes)return;this.routes=_.result(this,"routes");var route,routes=_.keys(this.routes);while((route=routes.pop())!=null){this.route(route,this.routes[route])}},_routeToRegExp:function(route){route=route.replace(escapeRegExp,"\\$&").replace(optionalParam,"(?:$1)?").replace(namedParam,function(match,optional){return optional?match:"([^/]+)"}).replace(splatParam,"(.*?)");return new RegExp("^"+route+"$")},_extractParameters:function(route,fragment){var params=route.exec(fragment).slice(1);return _.map(params,function(param){return param?decodeURIComponent(param):null})}});var History=Backbone.History=function(){this.handlers=[];_.bindAll(this,"checkUrl");if(typeof window!=="undefined"){this.location=window.location;this.history=window.history}};var routeStripper=/^[#\/]|\s+$/g;var rootStripper=/^\/+|\/+$/g;var isExplorer=/msie [\w.]+/;var trailingSlash=/\/$/;History.started=false;_.extend(History.prototype,Events,{interval:50,getHash:function(window){var match=(window||this).location.href.match(/#(.*)$/);return match?match[1]:""},getFragment:function(fragment,forcePushState){if(fragment==null){if(this._hasPushState||!this._wantsHashChange||forcePushState){fragment=this.location.pathname;var root=this.root.replace(trailingSlash,"");if(!fragment.indexOf(root))fragment=fragment.substr(root.length)}else{fragment=this.getHash()}}return fragment.replace(routeStripper,"")},start:function(options){if(History.started)throw new Error("Backbone.history has already been started");History.started=true;this.options=_.extend({},{root:"/"},this.options,options);this.root=this.options.root;this._wantsHashChange=this.options.hashChange!==false;this._wantsPushState=!!this.options.pushState;this._hasPushState=!!(this.options.pushState&&this.history&&this.history.pushState);var fragment=this.getFragment();var docMode=document.documentMode;var oldIE=isExplorer.exec(navigator.userAgent.toLowerCase())&&(!docMode||docMode<=7);this.root=("/"+this.root+"/").replace(rootStripper,"/");if(oldIE&&this._wantsHashChange){this.iframe=Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow;this.navigate(fragment)}if(this._hasPushState){Backbone.$(window).on("popstate",this.checkUrl)}else if(this._wantsHashChange&&"onhashchange"in window&&!oldIE){Backbone.$(window).on("hashchange",this.checkUrl)}else if(this._wantsHashChange){this._checkUrlInterval=setInterval(this.checkUrl,this.interval)}this.fragment=fragment;var loc=this.location;var atRoot=loc.pathname.replace(/[^\/]$/,"$&/")===this.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!atRoot){this.fragment=this.getFragment(null,true);this.location.replace(this.root+this.location.search+"#"+this.fragment);return true}else if(this._wantsPushState&&this._hasPushState&&atRoot&&loc.hash){this.fragment=this.getHash().replace(routeStripper,"");this.history.replaceState({},document.title,this.root+this.fragment+loc.search)}if(!this.options.silent)return this.loadUrl()},stop:function(){Backbone.$(window).off("popstate",this.checkUrl).off("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);History.started=false},route:function(route,callback){this.handlers.unshift({route:route,callback:callback})},checkUrl:function(e){var current=this.getFragment();if(current===this.fragment&&this.iframe){current=this.getFragment(this.getHash(this.iframe))}if(current===this.fragment)return false;if(this.iframe)this.navigate(current);this.loadUrl()||this.loadUrl(this.getHash())},loadUrl:function(fragmentOverride){var fragment=this.fragment=this.getFragment(fragmentOverride);var matched=_.any(this.handlers,function(handler){if(handler.route.test(fragment)){handler.callback(fragment);return true}});return matched},navigate:function(fragment,options){if(!History.started)return false;if(!options||options===true)options={trigger:options};fragment=this.getFragment(fragment||"");if(this.fragment===fragment)return;this.fragment=fragment;var url=this.root+fragment;if(this._hasPushState){this.history[options.replace?"replaceState":"pushState"]({},document.title,url)}else if(this._wantsHashChange){this._updateHash(this.location,fragment,options.replace);if(this.iframe&&fragment!==this.getFragment(this.getHash(this.iframe))){if(!options.replace)this.iframe.document.open().close();this._updateHash(this.iframe.location,fragment,options.replace)}}else{return this.location.assign(url)}if(options.trigger)this.loadUrl(fragment)},_updateHash:function(location,fragment,replace){if(replace){var href=location.href.replace(/(javascript:|#).*$/,"");location.replace(href+"#"+fragment)}else{location.hash="#"+fragment}}});Backbone.history=new History;var extend=function(protoProps,staticProps){var parent=this;var child;if(protoProps&&_.has(protoProps,"constructor")){child=protoProps.constructor}else{child=function(){return parent.apply(this,arguments)}}_.extend(child,parent,staticProps);var Surrogate=function(){this.constructor=child};Surrogate.prototype=parent.prototype;child.prototype=new Surrogate;if(protoProps)_.extend(child.prototype,protoProps);child.__super__=parent.prototype;return child};Model.extend=Collection.extend=Router.extend=View.extend=History.extend=extend;var urlError=function(){throw new Error('A "url" property or function must be specified')};var wrapError=function(model,options){var error=options.error;options.error=function(resp){if(error)error(model,resp,options);model.trigger("error",model,resp,options)}}}).call(this);\r
+/*\r
+//@ sourceMappingURL=backbone-min.map\r
+*/
\ No newline at end of file
index 0fc4437ee25805458a73a90f2d2c5bdd068120f8..5b26b69c377019ad7f389171788bcd894a136bf3 100644 (file)
@@ -12,6 +12,7 @@ html, body { height:100%; }
 #kiwi p { margin:0.5em 0; }
 #kiwi a { color:#36C; text-decoration:none; cursor:pointer; }
 #kiwi a img { border:none; }
+#kiwi .format_span a { color: inherit; background-color: inherit; text-decoration: inherit; font-style: inherit; font-weight: inherit;}
 
 
 /**
@@ -43,11 +44,17 @@ html, body { height:100%; }
 #kiwi #toolbar .panellist .alert_activity { font-weight: bold; }
 #kiwi #toolbar .panellist .alert_action { font-weight: bold; }
 
+#kiwi #toolbar .panellist li { width: auto; text-align: left; }
 #kiwi #toolbar .panellist li .part { position: absolute; top: 5px; right: 5px; }
 #kiwi #toolbar .panellist li .part:before { content:"[x]"; }
 #kiwi #toolbar .panellist li .part:hover { color: #900; }
 #kiwi #toolbar .panellist li img.icon { left:5px; top:2px; height:auto; width:auto; }
 
+#kiwi #toolbar .connections { overflow:hidden; }
+#kiwi #toolbar .connections .connection { float:left; }
+#kiwi #toolbar .connections .panellist { display:inline; }
+#kiwi #toolbar .connections .panellist:after { content:""; }
+
 #kiwi #status_message {
     background: #FFF;
     border-bottom: 1px solid;
@@ -104,9 +111,9 @@ html, body { height:100%; }
 #kiwi #memberlists ul li a.nick { }
 
 /* The userbox shown when clicking a nick */
-#kiwi #memberlists ul li .userbox { position:relative; }
-#kiwi #memberlists ul li .userbox a { }
-#kiwi #memberlists ul li .userbox a i { }
+#kiwi .userbox { position:relative; }
+#kiwi .userbox a { }
+#kiwi .userbox a i { }
 
 
 
@@ -159,7 +166,8 @@ html, body { height:100%; }
 /**
  * Server selection dialog
  */
-#kiwi .server_select { width:800px; margin:0 auto; overflow:hidden; }
+#kiwi .server_select { position:relative; width:320px; margin:0 auto; overflow:hidden; }
+#kiwi .server_select.initial { margin-top: 3em; }
 #kiwi .server_select .more { display: none; }
 #kiwi .server_select button {  }
 #kiwi .server_select input { }
@@ -170,9 +178,21 @@ html, body { height:100%; }
 #kiwi .server_select .basic { border-bottom: 1px solid gray; margin-bottom:1em; }
 #kiwi .server_select .basic .show_more { }
 #kiwi .server_select .basic tr.pass { display:none; }
+#kiwi .server_select .basic tr.key { display:none; }
 #kiwi .server_select.single_server .basic { border:none; }
 #kiwi .server_select .status {  }
 
+/* Channel key icon */
+#kiwi .server_select .basic tr.channel td { position: relative; }
+#kiwi .server_select .basic tr.channel .icon-key {
+    position: absolute;
+    top: 0.4em;
+    right: 0.6em;
+    font-size: 1.3em;
+    cursor: pointer;
+}
+#kiwi .server_select .basic tr.have_key { display:none; }
+
 /* When connected to an IRC server, .ok is set on the status div */
 #kiwi .server_select .status.ok { }
 
@@ -247,6 +267,30 @@ html, body { height:100%; }
 
 
 
+#kiwi .ui_menu {
+    padding: 0;
+    z-index:10;
+    position: absolute;
+    top: 100px; left: 100px;
+    background: #fff;
+    border: 1px solid #bbb;
+    background-clip: padding-box;
+}
+#kiwi .ui_menu .ui_menu_title {
+    padding: 5px 10px 5px 10px; font-weight: bold; overflow:hidden; background:#ddd;
+}
+#kiwi .ui_menu .ui_menu_content { padding: 5px 10px 5px 10px; border-top:1px solid #e9e9e9; overflow:hidden; position:relative; }
+#kiwi .ui_menu .ui_menu_content > a { display: block; }
+#kiwi .ui_menu .ui_menu_content > a[class^="icon-"]:before,
+#kiwi .ui_menu .ui_menu_content > a[class*=" icon-"]:before { margin-right: 5px; color:#666; }
+#kiwi .ui_menu .ui_menu_foot {
+    padding: 5px; border-top:1px solid #e9e9e9; background:#ddd; overflow:hidden;
+}
+#kiwi .ui_menu .ui_menu_foot .close { }
+
+
+
+
 
 /**
  * Themes
@@ -281,10 +325,10 @@ html, body { height:100%; }
     line-height: 1.4em;
     vertical-align: middle;
     
-    border-radius:5px;
     -moz-border-radius:5px;
     -webkit-border-radius:5px;
     -khtml-border-radius:5px;
+    border-radius:5px;
     behavior: url(border-radius.htc);
 
     background-image: -webkit-gradient(
@@ -308,7 +352,7 @@ html, body { height:100%; }
 #kiwi.theme_relaxed #toolbar .panellist .alert_activity { font-weight:normal; }
 #kiwi.theme_relaxed #toolbar .panellist .alert_action { font-weight:normal; }
 
-#kiwi.theme_relaxed #toolbar .panellist .active { padding-right:23px; }
+#kiwi.theme_relaxed #toolbar .panellist .active { padding-right:23px; border-top:2px solid #df6b26; border-bottom:none; }
 #kiwi.theme_relaxed #toolbar .panellist li .part:before { content:"\f00d"; }
 
 #kiwi.theme_relaxed #toolbar .panellist li.server span { padding-left: 5px; }
@@ -434,9 +478,9 @@ html, body { height:100%; }
 }
 #kiwi.theme_relaxed #memberlists ul li a.nick { display:block; color:black; }
 
-#kiwi.theme_relaxed #memberlists ul li .userbox { margin:4px 1em 5px 1em; padding-bottom:0.7em; font-size:.9em; }
-#kiwi.theme_relaxed #memberlists ul li .userbox a { display:block; text-decoration:none; margin-bottom:2px; }
-#kiwi.theme_relaxed #memberlists ul li .userbox a i { font-size:1.1em; margin-right:5px; }
+#kiwi.theme_relaxed .userbox { margin:4px 1em 5px 1em; padding-bottom:0.7em; font-size:.9em; }
+#kiwi.theme_relaxed .userbox a { display:block; text-decoration:none; margin-bottom:2px; }
+#kiwi.theme_relaxed .userbox a i { font-size:1.1em; margin-right:5px; }
 
 /* User mode styles */
 #kiwi.theme_relaxed #memberlists ul li .prefix {
@@ -534,7 +578,7 @@ html, body { height:100%; }
 
 
 /* The server select dialog */
-#kiwi.theme_relaxed .server_select { width:730px;  padding:3em 0 2em 0; margin: 0 auto; }
+#kiwi.theme_relaxed .server_select { margin-left:auto; margin-right:auto; }
 #kiwi.theme_relaxed .server_select .more { display: none; width:270px; margin:0 auto; }
 #kiwi.theme_relaxed .server_select table tr td { padding:5px; }
 #kiwi.theme_relaxed .server_select button { float:right; padding:3px 7px; }
@@ -545,6 +589,7 @@ html, body { height:100%; }
 #kiwi.theme_relaxed .server_select .basic input { width:170px; }
 #kiwi.theme_relaxed .server_select .basic label { font-size:1.3em; margin-top:4px; }
 #kiwi.theme_relaxed .server_select .basic tr.have_pass { font-size:0.8em; }
+#kiwi.theme_relaxed .server_select .basic tr.have_key { font-size:0.8em; }
 #kiwi.theme_relaxed .server_select .basic tr.channel td { padding-top:1em; }
 #kiwi.theme_relaxed .server_select .basic { border-bottom: 1px dashed gray; margin-bottom:1em; }
 #kiwi.theme_relaxed .server_select .basic .show_more { display: block; width:110px; margin:10px 0 0 0; font-size:0.8em; background: url(../img/more.png) no-repeat right 7px; }
@@ -557,13 +602,13 @@ html, body { height:100%; }
 }
 
 
-#kiwi.theme_relaxed .server_select .kiwi_logo { text-align: center; display:block; }
-#kiwi.theme_relaxed .server_select .kiwi_logo h1 {
-    font-size:20px;
-    line-height:48px; vertical-align: middle;
+#kiwi.theme_relaxed .server_select .kiwi_logo { margin-top:30px; }
+#kiwi.theme_relaxed .server_select .kiwi_logo h1 span {
+    font-size:14px;
+    line-height:24px; vertical-align: middle;
     color: #555555;
 }
-#kiwi.theme_relaxed .server_select .kiwi_logo img { }
+#kiwi.theme_relaxed .server_select .kiwi_logo img { display:inline; width:24px; }
 
 
 #kiwi.theme_relaxed.chanlist_treeview #panels { left:160px; }
@@ -597,6 +642,28 @@ html, body { height:100%; }
 }
 
 
+#kiwi.theme_relaxed .ui_menu {
+    border-radius: 3px;
+    color: #333;
+    box-shadow: 0 3px 8px rgba(0, 0, 0, .25);
+}
+#kiwi.theme_relaxed .ui_menu .ui_menu_title {
+    border-top-left-radius: 3px;
+    border-top-right-radius: 3px;
+}
+#kiwi.theme_relaxed .ui_menu .ui_menu_content { }
+#kiwi.theme_relaxed .ui_menu .ui_menu_content.hover:hover { background:#f7f7f7; }
+#kiwi.theme_relaxed .ui_menu .ui_menu_foot {
+    border-bottom-left-radius: 3px;
+    border-bottom-right-radius: 3px;
+}
+#kiwi.theme_relaxed .ui_menu .ui_menu_foot .close { float:right; font-size:0.9em; margin-right:1em; color: #999; }
+#kiwi.theme_relaxed .ui_menu .ui_menu_foot .close:hover { color: #222; }
+
+
+
+
+
 /**
  * Mini theme
  */
@@ -641,7 +708,7 @@ html, body { height:100%; }
 #kiwi.theme_mini #toolbar .panellist .alert_activity { font-weight:normal; }
 #kiwi.theme_mini #toolbar .panellist .alert_action { font-weight:normal; }
 
-#kiwi.theme_mini #toolbar .panellist .active { padding-right:23px; }
+#kiwi.theme_mini #toolbar .panellist .active { padding-right:23px; border-top:2px solid #df6b26; border-bottom:none; }
 #kiwi.theme_mini #toolbar .panellist li .part:before { content:"\f00d"; }
 
 #kiwi.theme_mini #toolbar .panellist li.server span { padding-left:5px; }
@@ -716,9 +783,9 @@ html, body { height:100%; }
 }
 #kiwi.theme_mini #memberlists ul li a.nick { display:block; color:black; }
 
-#kiwi.theme_mini #memberlists ul li .userbox { margin:0 1em 5px 1em; padding-bottom:0.7em; font-size:.9em; }
-#kiwi.theme_mini #memberlists ul li .userbox a { display:block; text-decoration:none; margin-bottom:2px; }
-#kiwi.theme_mini #memberlists ul li .userbox a i { font-size:1.1em; margin-right:5px; }
+#kiwi.theme_mini .userbox { margin:0 1em 5px 1em; padding-bottom:0.7em; font-size:.9em; }
+#kiwi.theme_mini .userbox a { display:block; text-decoration:none; margin-bottom:2px; }
+#kiwi.theme_mini .userbox a i { font-size:1.1em; margin-right:5px; }
 
 
 #kiwi.theme_mini #controlbox .input {
@@ -783,7 +850,7 @@ html, body { height:100%; }
 /* The server select dialog */
 #kiwi.theme_mini .server_select { padding:3em 0 2em 0; margin: 0 auto; width:100%; }
 #kiwi.theme_mini .server_select .more { display:none; }
-#kiwi.theme_mini .server_select button { display:block; padding:3px 7px; margin:0 auto; }
+#kiwi.theme_mini .server_select button { display:block; padding:3px 7px; margin:1em auto; }
 #kiwi.theme_mini .server_select input.nick {
     float:none; display:block; width:80%;
     padding:0.5em 1em; margin:0 auto;
@@ -796,6 +863,8 @@ html, body { height:100%; }
 #kiwi.theme_mini .server_select .basic table tr.channel,
 #kiwi.theme_mini .server_select .basic table tr.pass,
 #kiwi.theme_mini .server_select .basic table tr.have_pass { display:none; }
+#kiwi.theme_mini .server_select .basic table tr.key,
+#kiwi.theme_mini .server_select .basic table tr.have_key { display:none; }
 #kiwi.theme_mini .server_select .basic .show_more { display:none !important; }
 #kiwi.theme_mini .server_select.single_server .basic { border:none; }
 #kiwi.theme_mini .server_select .status { text-align: center; font-weight: bold; padding:1em; }
@@ -805,9 +874,13 @@ html, body { height:100%; }
     padding:0.5em; margin-top:1em; margin-bottom:1em; margin-right:2em;
 }
 
-#kiwi.theme_mini .server_select .divider-verticle {
-    display:none;
+#kiwi.theme_mini .server_select .kiwi_logo h1 span {
+    font-size:14px;
+    line-height:24px; vertical-align: middle;
+    color: #555555;
 }
+#kiwi.theme_mini .server_select .kiwi_logo img { display:inline; width:24px; }
+
 #kiwi.theme_mini .server_select .server_details {
     position: relative !important;
     float: none !important;
@@ -815,8 +888,6 @@ html, body { height:100%; }
     padding: 0 !important;
     margin: 2em 0 0 0 !important;
 }
-#kiwi.theme_mini .server_select .about_kiwi { display:none; }
-
 
 
 
@@ -903,7 +974,7 @@ html, body { height:100%; }
 #kiwi.theme_cli #toolbar .panellist .alert_activity { font-weight:normal; }
 #kiwi.theme_cli #toolbar .panellist .alert_action { font-weight:normal; }
 
-#kiwi.theme_cli #toolbar .panellist .active { padding-right:23px; }
+#kiwi.theme_cli #toolbar .panellist .active { padding-right:23px; border-top:2px solid #df6b26; border-bottom:none; }
 #kiwi.theme_cli #toolbar .panellist li .part:before { content:"\f00d"; }
 
 #kiwi.theme_cli #toolbar .panellist li.server span { padding-left:5px; }
@@ -933,10 +1004,10 @@ html, body { height:100%; }
     transition: 0.2s ease; 
 }
 
-#kiwi.theme_cli #memberlists ul li .userbox { margin:0 1em 0 1em; padding-bottom:0.4em; font-size:.9em; }
-#kiwi.theme_cli #memberlists ul li .userbox a { display:block; text-decoration:none; margin-bottom:2px; }
-#kiwi.theme_cli #memberlists ul li .userbox a i { font-size:1.1em; margin-right:5px; }
-#kiwi.theme_cli #memberlists ul li .userbox .divider-horizontal { display:none; }
+#kiwi.theme_cli .userbox { margin:0 1em 0 1em; padding-bottom:0.4em; font-size:.9em; }
+#kiwi.theme_cli .userbox a { display:block; text-decoration:none; margin-bottom:2px; }
+#kiwi.theme_cli .userbox a i { font-size:1.1em; margin-right:5px; }
+#kiwi.theme_cli .userbox .divider-horizontal { display:none; }
 
 
 #kiwi.theme_cli .messages .msg > div { color:#6d6d6d; font-family: Inconsolata, Consolas, 'courier new', monospace; }
@@ -1006,7 +1077,7 @@ html, body { height:100%; }
 
 
 /* The server select dialog */
-#kiwi.theme_cli .server_select { width:730px;  padding:3em 0 2em 0; margin: 0 auto; }
+#kiwi.theme_cli .server_select { margin-left:auto; margin-right:auto; color:#eee; }
 #kiwi.theme_cli .server_select .more { display: none; width:270px; margin:0 auto; }
 #kiwi.theme_cli .server_select table tr td { padding:5px; }
 #kiwi.theme_cli .server_select button { float:right; padding:3px 7px; margin-top:10px; }
@@ -1028,14 +1099,16 @@ html, body { height:100%; }
     padding:0.5em; margin-top:1em; margin-bottom:1em; margin-right:2em;
 }
 
+#kiwi.theme_cli .server_select .basic tr.channel .icon-key { color:#555; }
 
-#kiwi.theme_cli .server_select .kiwi_logo { text-align: center; display:block; }
+
+#kiwi.theme_cli .server_select .kiwi_logo { margin-top:30px; text-align:center; display:block; }
 #kiwi.theme_cli .server_select .kiwi_logo h1 {
     font-size:20px;
     line-height:48px; vertical-align: middle;
-    color: #555555;
+    color: #bbb;
 }
-#kiwi.theme_cli .server_select .kiwi_logo img { }
+#kiwi.theme_cli .server_select .kiwi_logo img { display:inline; width:24px; }
 
 
 #kiwi.theme_cli .divider-verticle {
@@ -1060,6 +1133,19 @@ html, body { height:100%; }
 #kiwi.theme_cli.chanlist_treeview #tabs ul li.active { padding-left:1em; }
 
 
+#kiwi.theme_cli .ui_menu {
+    color: #333;
+}
+#kiwi.theme_cli .ui_menu .ui_menu_title { }
+#kiwi.theme_cli .ui_menu .ui_menu_content { }
+#kiwi.theme_cli .ui_menu .ui_menu_content.hover:hover { background:#f7f7f7; }
+#kiwi.theme_cli .ui_menu .ui_menu_foot {
+    border-bottom-left-radius: 3px;
+    border-bottom-right-radius: 3px;
+}
+#kiwi.theme_cli .ui_menu .ui_menu_foot .close { float:right; font-size:0.9em; margin-right:1em; color: #999; }
+#kiwi.theme_cli .ui_menu .ui_menu_foot .close:hover { color: #222; }
+
 
 
 
@@ -1072,7 +1158,7 @@ html, body { height:100%; }
  * Basic theme
  */
 #kiwi.theme_basic {
-    background: #FFF;
+    background: url(../img/background-light.png) left top repeat-x #E3E3E3;
     color: #555555;
 }
 #kiwi.theme_basic,
@@ -1092,7 +1178,7 @@ html, body { height:100%; }
 }
 #kiwi.theme_basic #controlbox { background-color:#1B1B1B; }
 #kiwi.theme_basic #memberlists_resize_handle {
-    display:none; width:0;
+    /*display:none; width:0;*/
 }
 #kiwi.theme_basic #toolbar .panellist li {
     line-height: 1.4em;
@@ -1112,11 +1198,14 @@ html, body { height:100%; }
 #kiwi.theme_basic #toolbar .panellist .alert_activity { font-weight:normal; color:green; }
 #kiwi.theme_basic #toolbar .panellist .alert_action { font-weight:normal; color:green; }
 
-#kiwi.theme_basic #toolbar .panellist .active { padding-right:25px; }
+#kiwi.theme_basic #toolbar .panellist .active { padding-right:25px; border-top:2px solid #df6b26; border-bottom:none; }
 #kiwi.theme_basic #toolbar .panellist li .part {}
 #kiwi.theme_basic #toolbar .panellist li .part:before { content:"[x]"; }
 
-#kiwi.theme_basic #toolbar .panellist li.server span { }
+#kiwi.theme_basic #toolbar .panellist li.server span { padding-left: 5px; }
+#kiwi.theme_basic.connected #toolbar .panellist li.server:before { content: "\f0ec"; color:#3F9532; }
+#kiwi.theme_basic #toolbar .panellist li.server:before { content: "\f06a"; color:#900; font-size:1.5em; line-height:1em; vertical-align:middle; }
+
 
 /* Tab texts are within a span */
 #kiwi.theme_basic #toolbar .panellist li span  { line-height:20px; vertical-align:middle; display:inline-block; }
@@ -1140,7 +1229,7 @@ html, body { height:100%; }
 #kiwi.theme_basic .messages .msg { }
 #kiwi.theme_basic .messages .msg > div { font-family: Consolas, "Lucida Console", monospace; font-size:0.9em; }
 #kiwi.theme_basic .messages .msg { border: none; }
-#kiwi.theme_basic .messages .msg .time { display:inline; margin-right:1em; margin-left:2px; color:gray; }
+#kiwi.theme_basic .messages .msg .time { display:none; margin-right:1em; margin-left:2px; color:gray; }
 #kiwi.theme_basic .messages .msg .nick { display:inline; margin-right:1em; }
 #kiwi.theme_basic .messages .msg .nick:before { content:"<"; }
 #kiwi.theme_basic .messages .msg .nick:after { content:">"; }
@@ -1166,9 +1255,49 @@ html, body { height:100%; }
     padding:0.5em; margin-top:1em; margin-bottom:1em; margin-right:2em;
 }
 
+#kiwi.theme_basic.timestamps .messages .msg .time { display:inline; }
+#kiwi.theme_basic.timestamps .messages .msg .text { }
+
 #kiwi.theme_basic .messages .msg.global_nick_highlight,
 #kiwi.theme_basic .messages .msg.highlight { background:#D9D9D9; }
 
+/* Narrow styling (window width < 400px) */
+#kiwi.theme_basic.narrow .messages .msg .nick { width: auto; }
+#kiwi.theme_basic.narrow .messages .msg .text { margin-left: 1em; border:none; }
+#kiwi.theme_basic.narrow .messages .msg.action .text { margin-left: 1em; }
+
+#kiwi.theme_basic .messages .msg .media { margin-left:0.5em; }
+#kiwi.theme_basic .messages .msg .media a { text-decoration:none; }
+#kiwi.theme_basic .messages .msg .media .media_close { font-size:0.9em; }
+#kiwi.theme_basic .messages .msg .media .media_content { margin:10px 0 0 2em; overflow:hidden; }
+#kiwi.theme_basic .messages .msg .media .media_content img { padding:3px; border:1px solid gray; }
+#kiwi.theme_basic .messages .msg .media .media_content > .content {
+    background: white;
+    overflow: hidden;
+    padding: 10px;
+    border: #DDD 1px solid;
+    border-top-color: #EEE;
+    border-bottom-color: #BBB;
+    -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
+    -moz-box-shadow: 0 1px 3px rgba(0,0,0,0.15);
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
+    border-radius: 5px;
+    float: left;
+}
+
+#kiwi.theme_basic .messages .msg .media.twitter .media_content > .content {
+    background: transparent;
+    border:none;
+    overflow: hidden;
+    box-shadow: none;
+    padding: 0;
+}
+#kiwi.theme_basic .messages .msg .media.reddit .thumbnail_nsfw {
+    display: inline-block;
+    float: left;
+}
+#kiwi.theme_basic .messages .msg .media.reddit .thumbnail { float:left; margin-right: 0.5em; }
+
 
 
 #kiwi.theme_basic #memberlists {
@@ -1181,9 +1310,9 @@ html, body { height:100%; }
 #kiwi.theme_basic #memberlists ul li:hover {}
 #kiwi.theme_basic #memberlists ul li a.nick { display:block; color:black; }
 
-#kiwi.theme_basic #memberlists ul li .userbox { margin:0 1em 5px 1em; padding-bottom:0.7em; font-size:.9em; }
-#kiwi.theme_basic #memberlists ul li .userbox a { display:block; text-decoration:none; margin-bottom:2px; }
-#kiwi.theme_basic #memberlists ul li .userbox a i { font-size:1.1em; margin-right:5px; }
+#kiwi.theme_basic .userbox { margin:0 1em 5px 1em; padding-bottom:0.7em; font-size:.9em; }
+#kiwi.theme_basic .userbox a { display:block; text-decoration:none; margin-bottom:2px; }
+#kiwi.theme_basic .userbox a i { font-size:1.1em; margin-right:5px; }
 
 
 #kiwi.theme_basic #controlbox .input {
@@ -1215,6 +1344,7 @@ html, body { height:100%; }
     position:relative;
     height:100%; width:100%;
     display: block;
+    outline: none;
 }
 
 
@@ -1263,7 +1393,7 @@ html, body { height:100%; }
 
 
 /* The server select dialog */
-#kiwi.theme_basic .server_select { width:730px;  padding:3em 0 2em 0; margin: 0 auto; }
+#kiwi.theme_basic .server_select { margin-left:auto; margin-right:auto; }
 #kiwi.theme_basic .server_select a { text-decoration: none; }
 #kiwi.theme_basic .server_select .more { display: none; width:270px; margin:0 auto; }
 #kiwi.theme_basic .server_select table tr td { padding:5px; }
@@ -1287,13 +1417,13 @@ html, body { height:100%; }
 }
 
 
-#kiwi.theme_basic .server_select .kiwi_logo { text-align: center; display:block; }
+#kiwi.theme_basic .server_select .kiwi_logo { margin-top:30px; text-align:center; display:block; }
 #kiwi.theme_basic .server_select .kiwi_logo h1 {
-    font-size:20px;
+    font-size:14px;
     line-height:48px; vertical-align: middle;
     color: #555555;
 }
-#kiwi.theme_basic .server_select .kiwi_logo img { }
+#kiwi.theme_basic .server_select .kiwi_logo img { display:inline; width:24px; }
 
 
 #kiwi.theme_basic.chanlist_treeview #panels { left:160px; }
@@ -1309,3 +1439,7 @@ html, body { height:100%; }
     border-bottom-right-radius:0;
     border-top-right-radius:0;
 }
+
+
+#kiwi.theme_basic .ui_menu .ui_menu_foot .close { float:right; font-size:0.9em; margin-right:1em; color: #999; }
+#kiwi.theme_basic .ui_menu .ui_menu_foot .close:hover { color: #222; }
index e14e42f8488590c58046d06a1b110a54384fdaf8..7eeeae0fdc21603e5240d85c1bef629ac8a48551 100644 (file)
@@ -26,38 +26,59 @@ _kiwi.global = {
 \r
        // Event managers for plugins\r
        components: {\r
-           EventComponent: function(event_source) {\r
+           EventComponent: function(event_source, proxy_event_name) {\r
                function proxyEvent(event_name, event_data) {\r
+                       if (proxy_event_name !== 'all') {\r
+                               event_data = event_name.event_data;\r
+                               event_name = event_name.event_name\r
+                       }\r
+//console.log(proxy_event_name, event_name, event_data);\r
                    this.trigger(event_name, event_data);\r
                }\r
 \r
+               // The event we are to proxy\r
+               proxy_event_name = proxy_event_name || 'all';\r
+\r
+\r
                _.extend(this, Backbone.Events);\r
                this._source = event_source;\r
 \r
                // Proxy the events to this dispatcher\r
-               event_source.on('all', proxyEvent, this);\r
+               event_source.on(proxy_event_name, proxyEvent, this);\r
 \r
                // Clean up this object\r
                this.dispose = function () {\r
-                   event_source.off('all', proxyEvent);\r
+                   event_source.off(proxy_event_name, proxyEvent);\r
                    this.off();\r
                    delete this.event_source;\r
                };\r
            },\r
 \r
-           Network: function() {\r
-               var obj = new this.EventComponent(_kiwi.gateway);\r
+           Network: function(connection_id) {\r
+               var connection_event;\r
+\r
+               if (typeof connection_id !== 'undefined') {\r
+                       connection_event = 'connection:' + connection_id.toString();\r
+               }\r
+\r
+               var obj = new this.EventComponent(_kiwi.gateway, connection_event);\r
                var funcs = {\r
                        kiwi: 'kiwi', raw: 'raw', kick: 'kick', topic: 'topic',\r
                        part: 'part', join: 'join', action: 'action', ctcp: 'ctcp',\r
-                       notice: 'notice', msg: 'privmsg',\r
-                       get: 'get'\r
+                       notice: 'notice', msg: 'privmsg', changeNick: 'changeNick'\r
                };\r
 \r
+               // Proxy each gateway method\r
                _.each(funcs, function(gateway_fn, func_name) {\r
                        obj[func_name] = function() {\r
                                var fn_name = gateway_fn;\r
-                               return _kiwi.gateway[fn_name].apply(_kiwi.gateway, arguments);\r
+\r
+                               // Add connection_id to the argument list\r
+                               var args = Array.prototype.slice.call(arguments, 0);\r
+                               args.unshift(connection_id);\r
+\r
+                               // Call the gateway function on behalf of this connection\r
+                               return _kiwi.gateway[fn_name].apply(_kiwi.gateway, args);\r
                        };\r
                });\r
 \r
index e856a85d8401014dd471e89d123e7bd804c3793d..dd68457033c50e5d53abb7969c843a3f3c408ba3 100644 (file)
@@ -32,10 +32,13 @@ var src = concat([
     __dirname + '/app.js',\r
     __dirname + '/model_application.js',\r
     __dirname + '/model_gateway.js',\r
+    __dirname + '/model_network.js',\r
     __dirname + '/model_member.js',\r
     __dirname + '/model_memberlist.js',\r
+    __dirname + '/model_newconnection.js',\r
     __dirname + '/model_panel.js',\r
     __dirname + '/model_panellist.js',\r
+    __dirname + '/model_networkpanellist.js',\r
     __dirname + '/model_query.js',\r
     __dirname + '/model_channel.js',\r
     __dirname + '/model_server.js',\r
index bff13ff8d2852df246cb5f5f80781ab0c72fa408..0721a9e5d7cfec43731863f97e0de16440c2dcf5 100644 (file)
                 </ul>\r
             </div>\r
 \r
-            <div id="tabs">\r
-                <ul class="panellist channels"></ul>\r
-                <ul class="panellist applets"></ul>\r
-            </div>\r
+            <div id="tabs"></div>\r
 \r
             <div id="topic">\r
                 <div contenteditable="true"></div>\r
@@ -41,7 +38,7 @@
             <div class="panel_container container1"></div>\r
         </div>\r
 \r
-        <div id="memberlists"></div>\r
+        <div id="memberlists" class="disabled"></div>\r
 \r
         <div id="controlbox">\r
             <div class="input">\r
@@ -69,8 +66,6 @@
             <a class="query"><i class="icon-comment"></i>Message</a>\r
             <a class="info"><i class="icon-info-sign"></i>Info</a>\r
             <a class="slap"><i class="icon-user-md"></i>Slap!</a>\r
-\r
-            <div class="divider-horizontal"></div>\r
         </div>\r
     </script>\r
     \r
         </form>\r
     </script>\r
 \r
+    <script type="text/html" id="tmpl_new_connection_info">\r
+        <div style="margin:1em 20px;">\r
+            <img src="https://kiwiirc.com/img/ico.png" alt="KiwiIRC Logo" title="Kiwi IRC" style="display:block; margin:0 auto;"/> <br />\r
+            <p style="font-style:italic;">A <strong>hand-crafted IRC client</strong> that you can enjoy. Designed to be used <strong>easily</strong> and <strong>freely</strong>.</p>\r
+\r
+            <p style="font-size:0.9em;margin-top:2em;">Peek at the <a href="https://www.kiwiirc.com/">Kiwi IRC homepage</a> for more information or to find out how to embed it on your own website. Looking for source code? Try the <a href="http://github.com/prawnsalad/KiwiIRC/">GitHub</a> page. This network of people may not be associated with Kiwi IRC itself.</p>\r
+        </div>\r
+    </script>\r
+\r
     <script type="text/html" id="tmpl_server_select">\r
         <div class="server_select">\r
 \r
-            <div class="server_details" style="position:relative;float:left;width:320px;padding-right:3em;margin-top:50px;">\r
+            <div class="side_panel" style="position:absolute;top:0px;left:320px;">\r
+                <div class="content" style="position:relative;width:300px;">\r
+                </div>\r
+            </div>\r
+\r
+            <div class="server_details" style="position:relative;width:320px;">\r
                 <div class="status">Think of a nickname..</div>\r
 \r
                 <form>\r
 \r
                             <tr class="channel">\r
                                 <td><label for="server_select_channel">Channel</label></td>\r
-                                <td><input type="text" class="channel" id="server_select_channel"><input type="hidden" class="channel_key"></td>\r
+                                <td>\r
+                                    <div style="position:relative;">\r
+                                        <input type="text" class="channel" id="server_select_channel">\r
+                                        <i class="icon-key" title="Channel Key"></i>\r
+                                    </div>\r
+                                </td>\r
+                            </tr>\r
+\r
+                            <tr class="have_key">\r
+                                <td colspan="2">\r
+                                    <label for="server_select_show_channel_key">Channel requires a key</label> <input type="checkbox" id="server_select_show_channel_key" style="width:auto;" />\r
+                                </td>\r
+                            </tr>\r
+\r
+                            <tr class="key">\r
+                                <td><label for="server_select_channel_key">Key</label></td>\r
+                                <td><input type="password" class="channel_key" id="server_select_channel_key"></td>\r
                             </tr>\r
 \r
                             <tr class="start">\r
                     </div>\r
                 </form>\r
 \r
-                <div class="divider-verticle"></div>\r
-            </div>\r
-\r
-            <div class="about_kiwi" style="position:relative;float:left;width:320px;margin-left:3em;color:#555555;">\r
                 <a class="kiwi_logo" href="https://kiwiirc.com/" target="_blank">\r
-                    <img src="<%base_path%>/assets/img/ico.png" alt="KiwiIRC Logo" title="Kiwi IRC" /> <br />\r
-                    <h1>Powered by Kiwi IRC</h1>\r
+                    <h1><span>Powered by Kiwi IRC</span> <img src="<%base_path%>/assets/img/ico.png" alt="KiwiIRC Logo" title="Kiwi IRC" /></h1>\r
                 </a>\r
-\r
-                <p style="font-style:italic;">A <strong>hand-crafted IRC client</strong> that you can enjoy. Designed to be used <strong>easily</strong> and <strong>freely</strong>.</p>\r
-\r
-                <p style="font-size:0.9em;margin-top:2em;">Peek at the <a href="https://kiwiirc.com/">Kiwi IRC homepage</a> for more information or to find out how to embed it on your own website. Looking for source code? Try the <a href="http://github.com/prawnsalad/KiwiIRC/">GitHub</a> page. This network of people may not be associated with Kiwi IRC itself.</p>\r
             </div>\r
         </div>\r
     </script>\r
                     'dev/model_gateway.js'\r
                 ],\r
                 [\r
+                    'dev/model_newconnection.js',\r
                     'dev/model_panellist.js',\r
+                    'dev/model_networkpanellist.js',\r
                     'dev/model_panel.js',\r
                     'dev/model_member.js',\r
-                    'dev/model_memberlist.js'\r
+                    'dev/model_memberlist.js',\r
+                    'dev/model_network.js'\r
                 ],\r
                 \r
                 [\r
     };\r
 </script>\r
 </body>\r
-</html>
\ No newline at end of file
+</html>\r
index 90204e8af337ac9c8bbcc3f7448a245852fdd951..0dca6377fd21776d18a62797659fcdd47174956d 100644 (file)
@@ -91,7 +91,7 @@ _kiwi.model.Applet = _kiwi.model.Panel.extend({
     loadOnce: function (applet_name) {\r
 \r
         // See if we have an instance loaded already\r
-        var applet = _.find(_kiwi.app.panels.models, function(panel) {\r
+        var applet = _.find(_kiwi.app.panels('applets'), function(panel) {\r
             // Ignore if it's not an applet\r
             if (!panel.isApplet()) return;\r
 \r
@@ -122,7 +122,7 @@ _kiwi.model.Applet = _kiwi.model.Panel.extend({
         applet.load(new _kiwi.applets[applet_name]({_applet_name: applet_name}));\r
 \r
         // Add it into the tab list\r
-        _kiwi.app.panels.add(applet);\r
+        _kiwi.app.applet_panels.add(applet);\r
 \r
 \r
         return applet;\r
index 7cb98e98a1a09a47460f5667b0f6b7ce6b97b499..0fe66b1d18b8b9226cdac55497c1877360029e9f 100644 (file)
@@ -7,9 +7,6 @@ _kiwi.model.Application = function () {
 \r
 \r
     var model = function () {\r
-        /** Instance of _kiwi.model.PanelList */\r
-        this.panels = null;\r
-\r
         /** _kiwi.view.Application */\r
         this.view = null;\r
 \r
@@ -34,8 +31,12 @@ _kiwi.model.Application = function () {
 \r
             // Best guess at where the kiwi server is\r
             this.detectKiwiServer();\r
+\r
+            // Takes instances of model_network\r
+            this.connections = new _kiwi.model.NetworkPanelList();\r
         };\r
 \r
+\r
         this.start = function () {\r
             // Only debug if set in the querystring\r
             if (!getQueryVariable('debug')) {\r
@@ -43,7 +44,7 @@ _kiwi.model.Application = function () {
             } else {\r
                 //manageDebug(true);\r
             }\r
-            \r
+\r
             // Set the gateway up\r
             _kiwi.gateway = new _kiwi.model.Gateway();\r
             this.bindGatewayCommands(_kiwi.gateway);\r
@@ -53,45 +54,10 @@ _kiwi.model.Application = function () {
 \r
             this.view.barsHide(true);\r
 \r
-            this.panels.server.server_login.bind('server_connect', function (event) {\r
-                var server_login = this,\r
-                    transport_path = '';\r
-                auto_connect_details = event;\r
-\r
-                server_login.networkConnecting();\r
-                \r
-                // Path to get the socket.io transport code\r
-                transport_path = that.kiwi_server + that.get('base_path') + '/transport/socket.io.js?ts='+(new Date().getTime());\r
-                \r
-                $script(transport_path, function () {\r
-                    if (!window.io) {\r
-                        kiwiServerNotFound();\r
-                        return;\r
-                    }\r
-                    _kiwi.gateway.set('kiwi_server', that.kiwi_server + '/kiwi');\r
-                    _kiwi.gateway.set('nick', event.nick);\r
-                    \r
-                    _kiwi.gateway.connect(event.server, event.port, event.ssl, event.password, function (error) {\r
-                        if (error) {\r
-                            kiwiServerNotFound();\r
-                        }\r
-                    });\r
-                });\r
-            });\r
-\r
-            // TODO: Shouldn't really be here but it's not working in the view.. :/\r
-            // Hack for firefox browers: Focus is not given on this event loop iteration\r
-            setTimeout(function(){\r
-                _kiwi.app.panels.server.server_login.$el.find('.nick').select();\r
-            }, 0);\r
+            this.showIntialConenctionDialog();\r
         };\r
 \r
 \r
-        function kiwiServerNotFound (e) {\r
-            that.panels.server.server_login.showError();\r
-        }\r
-\r
-\r
         this.detectKiwiServer = function () {\r
             // If running from file, default to localhost:7777 by default\r
             if (window.location.protocol === 'file:') {\r
@@ -103,14 +69,51 @@ _kiwi.model.Application = function () {
         };\r
 \r
 \r
+        this.showIntialConenctionDialog = function() {\r
+            var connection_dialog = new _kiwi.model.NewConnection();\r
+            this.populateDefaultServerSettings(connection_dialog);\r
+\r
+            connection_dialog.view.$el.addClass('initial');\r
+            this.view.$el.find('.panel_container:first').append(connection_dialog.view.$el);\r
+\r
+            var $info = $($('#tmpl_new_connection_info').html());\r
+\r
+            if ($info.html()) {\r
+                connection_dialog.view.infoBoxSet($info);\r
+                connection_dialog.view.infoBoxShow();\r
+            }\r
+\r
+            // TODO: Shouldn't really be here but it's not working in the view.. :/\r
+            // Hack for firefox browers: Focus is not given on this event loop iteration\r
+            setTimeout(function(){\r
+                connection_dialog.view.$el.find('.nick').select();\r
+            }, 0);\r
+\r
+            // Once connected, close this dialog and remove its own event\r
+            var fn = function() {\r
+                connection_dialog.view.$el.slideUp(function() {\r
+                    connection_dialog.view.dispose();\r
+                    connection_dialog = null;\r
+\r
+                    _kiwi.gateway.off('onconnect', fn);\r
+                });\r
+\r
+            };\r
+            _kiwi.gateway.on('onconnect', fn);\r
+        };\r
+\r
+\r
         this.initializeClient = function () {\r
             this.view = new _kiwi.view.Application({model: this, el: this.get('container')});\r
-            \r
+\r
+            // Applets panel list\r
+            this.applet_panels = new _kiwi.model.PanelList();\r
+            this.applet_panels.view.$el.addClass('panellist applets');\r
+            this.view.$el.find('#tabs').append(this.applet_panels.view.$el);\r
+\r
             /**\r
              * Set the UI components up\r
              */\r
-            this.panels = new _kiwi.model.PanelList();\r
-\r
             this.controlbox = new _kiwi.view.ControlBox({el: $('#controlbox')[0]});\r
             this.bindControllboxCommands(this.controlbox);\r
 \r
@@ -121,32 +124,31 @@ _kiwi.model.Application = function () {
             this.message = new _kiwi.view.StatusMessage({el: $('#status_message')[0]});\r
 \r
             this.resize_handle = new _kiwi.view.ResizeHandler({el: $('#memberlists_resize_handle')[0]});\r
-            \r
-            this.panels.server.view.show();\r
 \r
             // Rejigg the UI sizes\r
             this.view.doLayout();\r
-\r
-            this.populateDefaultServerSettings();\r
         };\r
 \r
 \r
         this.initializeGlobals = function () {\r
+            _kiwi.global.connections = this.connections;\r
+\r
             _kiwi.global.panels = this.panels;\r
-            \r
+            _kiwi.global.panels.applets = this.applet_panels;\r
+\r
             _kiwi.global.components.Applet = _kiwi.model.Applet;\r
             _kiwi.global.components.Panel =_kiwi.model.Panel;\r
         };\r
 \r
 \r
-        this.populateDefaultServerSettings = function () {\r
+        this.populateDefaultServerSettings = function (new_connection_dialog) {\r
             var parts;\r
             var defaults = {\r
-                nick: getQueryVariable('nick') || '',\r
+                nick: '',\r
                 server: '',\r
                 port: 6667,\r
                 ssl: false,\r
-                channel: window.location.hash || '#chat',\r
+                channel: '#chat',\r
                 channel_key: ''\r
             };\r
             var uricheck;\r
@@ -154,7 +156,7 @@ _kiwi.model.Application = function () {
 \r
             /**\r
              * Get any settings set by the server\r
-             * These settings may be changed in the server selection dialog\r
+             * These settings may be changed in the server selection dialog or via URL parameters\r
              */\r
             if (this.server_settings.client) {\r
                 if (this.server_settings.client.nick)\r
@@ -180,6 +182,14 @@ _kiwi.model.Application = function () {
              * These settings may be changed in the server selection dialog\r
              */\r
 \r
+            // Any query parameters first\r
+            if (getQueryVariable('nick'))\r
+                defaults.nick = getQueryVariable('nick');\r
+\r
+            if (window.location.hash)\r
+                defaults.channel = window.location.hash;\r
+\r
+\r
             // Process the URL part by part, extracting as we go\r
             parts = window.location.pathname.toString().replace(this.get('base_path'), '').split('/');\r
 \r
@@ -276,19 +286,45 @@ _kiwi.model.Application = function () {
             defaults.nick = defaults.nick.replace('?', Math.floor(Math.random() * 100000).toString());\r
 \r
             // Populate the server select box with defaults\r
-            this.panels.server.server_login.populateFields(defaults);\r
+            new_connection_dialog.view.populateFields(defaults);\r
         };\r
 \r
 \r
-        this.bindGatewayCommands = function (gw) {\r
-            gw.on('onmotd', function (event) {\r
-                that.panels.server.addMsg(_kiwi.gateway.get('name'), event.msg, 'motd');\r
-            });\r
+        this.panels = (function() {\r
+            var fn = function(panel_type) {\r
+                var panels;\r
+\r
+                // Default panel type\r
+                panel_type = panel_type || 'connections';\r
+\r
+                switch (panel_type) {\r
+                case 'connections':\r
+                    panels = this.connections.panels();\r
+                    break;\r
+                case 'applets':\r
+                    panels = this.applet_panels.models;\r
+                    break;\r
+                }\r
 \r
+                // Active panels / server\r
+                panels.active = this.connections.active_panel;\r
+                panels.server = this.connections.active_connection ?\r
+                    this.connections.active_connection.panels.server :\r
+                    null;\r
+\r
+                return panels;\r
+            };\r
 \r
+            _.extend(fn, Backbone.Events);\r
+\r
+            return fn;\r
+        })();\r
+\r
+\r
+        this.bindGatewayCommands = function (gw) {\r
             gw.on('onconnect', function (event) {\r
                 that.view.barsShow();\r
-                \r
+\r
                 if (auto_connect_details.channel) {\r
                     that.controlbox.processInput('/JOIN ' + auto_connect_details.channel + ' ' + auto_connect_details.channel_key);\r
                 }\r
@@ -305,17 +341,17 @@ _kiwi.model.Application = function () {
                     that.view.$el.removeClass('connected');\r
 \r
                     // Mention the disconnection on every channel\r
-                    $.each(_kiwi.app.panels.models, function (idx, panel) {\r
+                    $.each(_kiwi.app.connections.getByConnectionId(0).panels.models, function (idx, panel) {\r
                         if (!panel || !panel.isChannel()) return;\r
                         panel.addMsg('', msg, 'action quit');\r
                     });\r
-                    _kiwi.app.panels.server.addMsg('', msg, 'action quit');\r
+                    _kiwi.app.connections.getByConnectionId(0).panels.server.addMsg('', msg, 'action quit');\r
 \r
                     gw_stat = 1;\r
                 });\r
                 gw.on('reconnecting', function (event) {\r
                     msg = 'You have been disconnected. Attempting to reconnect again in ' + (event.delay/1000) + ' seconds..';\r
-                    _kiwi.app.panels.server.addMsg('', msg, 'action quit');\r
+                    _kiwi.app.connections.getByConnectionId(0).panels.server.addMsg('', msg, 'action quit');\r
                 });\r
                 gw.on('onconnect', function (event) {\r
                     that.view.$el.addClass('connected');\r
@@ -325,463 +361,16 @@ _kiwi.model.Application = function () {
                     that.message.text(msg, {timeout: 5000});\r
 \r
                     // Mention the disconnection on every channel\r
-                    $.each(_kiwi.app.panels.models, function (idx, panel) {\r
+                    $.each(_kiwi.app.connections.getByConnectionId(0).panels.models, function (idx, panel) {\r
                         if (!panel || !panel.isChannel()) return;\r
                         panel.addMsg('', msg, 'action join');\r
                     });\r
-                    _kiwi.app.panels.server.addMsg('', msg, 'action join');\r
+                    _kiwi.app.connections.getByConnectionId(0).panels.server.addMsg('', msg, 'action join');\r
 \r
                     gw_stat = 0;\r
                 });\r
             })();\r
 \r
-\r
-            gw.on('onjoin', function (event) {\r
-                var c, members, user;\r
-                c = that.panels.getByName(event.channel);\r
-                if (!c) {\r
-                    c = new _kiwi.model.Channel({name: event.channel});\r
-                    that.panels.add(c);\r
-                }\r
-\r
-                members = c.get('members');\r
-                if (!members) return;\r
-\r
-                user = new _kiwi.model.Member({nick: event.nick, ident: event.ident, hostname: event.hostname});\r
-                members.add(user);\r
-                // TODO: highlight the new channel in some way\r
-            });\r
-\r
-\r
-            gw.on('onpart', function (event) {\r
-                var channel, members, user,\r
-                    part_options = {};\r
-\r
-                part_options.type = 'part';\r
-                part_options.message = event.message || '';\r
-\r
-                channel = that.panels.getByName(event.channel);\r
-                if (!channel) return;\r
-\r
-                // If this is us, close the panel\r
-                if (event.nick === _kiwi.gateway.get('nick')) {\r
-                    channel.close();\r
-                    return;\r
-                }\r
-\r
-                members = channel.get('members');\r
-                if (!members) return;\r
-\r
-                user = members.getByNick(event.nick);\r
-                if (!user) return;\r
-\r
-                members.remove(user, part_options);\r
-            });\r
-\r
-\r
-            gw.on('onquit', function (event) {\r
-                var member, members,\r
-                    quit_options = {};\r
-\r
-                quit_options.type = 'quit';\r
-                quit_options.message = event.message || '';\r
-\r
-                $.each(that.panels.models, function (index, panel) {\r
-                    if (!panel.isChannel()) return;\r
-\r
-                    member = panel.get('members').getByNick(event.nick);\r
-                    if (member) {\r
-                        panel.get('members').remove(member, quit_options);\r
-                    }\r
-                });\r
-            });\r
-\r
-\r
-            gw.on('onkick', function (event) {\r
-                var channel, members, user,\r
-                    part_options = {};\r
-\r
-                part_options.type = 'kick';\r
-                part_options.by = event.nick;\r
-                part_options.message = event.message || '';\r
-\r
-                channel = that.panels.getByName(event.channel);\r
-                if (!channel) return;\r
-\r
-                members = channel.get('members');\r
-                if (!members) return;\r
-\r
-                user = members.getByNick(event.kicked);\r
-                if (!user) return;\r
-\r
-                members.remove(user, part_options);\r
-\r
-                if (event.kicked === _kiwi.gateway.get('nick')) {\r
-                    members.reset([]);\r
-                }\r
-                \r
-            });\r
-\r
-\r
-            gw.on('onmsg', function (event) {\r
-                var panel,\r
-                    is_pm = (event.channel == _kiwi.gateway.get('nick'));\r
-\r
-                // An ignored user? don't do anything with it\r
-                if (gw.isNickIgnored(event.nick)) {\r
-                    return;\r
-                }\r
-\r
-                if (is_pm) {\r
-                    // If a panel isn't found for this PM, create one\r
-                    panel = that.panels.getByName(event.nick);\r
-                    if (!panel) {\r
-                        panel = new _kiwi.model.Query({name: event.nick});\r
-                        that.panels.add(panel);\r
-                    }\r
-\r
-                } else {\r
-                    // If a panel isn't found for this channel, reroute to the\r
-                    // server panel\r
-                    panel = that.panels.getByName(event.channel);\r
-                    if (!panel) {\r
-                        panel = that.panels.server;\r
-                    }\r
-                }\r
-\r
-                panel.addMsg(event.nick, event.msg);\r
-            });\r
-\r
-\r
-            gw.on('onctcp_request', function (event) {\r
-                // An ignored user? don't do anything with it\r
-                if (gw.isNickIgnored(event.nick)) {\r
-                    return;\r
-                }\r
-\r
-                // Reply to a TIME ctcp\r
-                if (event.msg.toUpperCase() === 'TIME') {\r
-                    gw.ctcp(false, event.type, event.nick, (new Date()).toString());\r
-                }\r
-            });\r
-\r
-\r
-            gw.on('onctcp_response', function (event) {\r
-                // An ignored user? don't do anything with it\r
-                if (gw.isNickIgnored(event.nick)) {\r
-                    return;\r
-                }\r
-                \r
-                that.panels.server.addMsg('[' + event.nick + ']', 'CTCP ' + event.msg);\r
-            });\r
-\r
-\r
-            gw.on('onnotice', function (event) {\r
-                var panel;\r
-\r
-                // An ignored user? don't do anything with it\r
-                if (event.nick && gw.isNickIgnored(event.nick)) {\r
-                    return;\r
-                }\r
-\r
-                // Find a panel for the destination(channel) or who its from\r
-                panel = that.panels.getByName(event.target) || that.panels.getByName(event.nick);\r
-                if (!panel) {\r
-                    panel = that.panels.server;\r
-                }\r
-\r
-                panel.addMsg('[' + (event.nick||'') + ']', event.msg);\r
-            });\r
-\r
-\r
-            gw.on('onaction', function (event) {\r
-                var panel,\r
-                    is_pm = (event.channel == _kiwi.gateway.get('nick'));\r
-\r
-                // An ignored user? don't do anything with it\r
-                if (gw.isNickIgnored(event.nick)) {\r
-                    return;\r
-                }\r
-\r
-                if (is_pm) {\r
-                    // If a panel isn't found for this PM, create one\r
-                    panel = that.panels.getByName(event.nick);\r
-                    if (!panel) {\r
-                        panel = new _kiwi.model.Channel({name: event.nick});\r
-                        that.panels.add(panel);\r
-                    }\r
-\r
-                } else {\r
-                    // If a panel isn't found for this channel, reroute to the\r
-                    // server panel\r
-                    panel = that.panels.getByName(event.channel);\r
-                    if (!panel) {\r
-                        panel = that.panels.server;\r
-                    }\r
-                }\r
-\r
-                panel.addMsg('', '* ' + event.nick + ' ' + event.msg, 'action');\r
-            });\r
-\r
-\r
-            gw.on('ontopic', function (event) {\r
-                var c;\r
-                c = that.panels.getByName(event.channel);\r
-                if (!c) return;\r
-\r
-                // Set the channels topic\r
-                c.set('topic', event.topic);\r
-\r
-                // If this is the active channel, update the topic bar too\r
-                if (c.get('name') === _kiwi.app.panels.active.get('name')) {\r
-                    that.topicbar.setCurrentTopic(event.topic);\r
-                }\r
-            });\r
-\r
-\r
-            gw.on('ontopicsetby', function (event) {\r
-                var c, when;\r
-                c = that.panels.getByName(event.channel);\r
-                if (!c) return;\r
-\r
-                when = formatDate(new Date(event.when * 1000));\r
-                c.addMsg('', 'Topic set by ' + event.nick + ' at ' + when, 'topic');\r
-            });\r
-\r
-\r
-            gw.on('onuserlist', function (event) {\r
-                var channel;\r
-                channel = that.panels.getByName(event.channel);\r
-\r
-                // If we didn't find a channel for this, may aswell leave\r
-                if (!channel) return;\r
-\r
-                channel.temp_userlist = channel.temp_userlist || [];\r
-                _.each(event.users, function (item) {\r
-                    var user = new _kiwi.model.Member({nick: item.nick, modes: item.modes});\r
-                    channel.temp_userlist.push(user);\r
-                });\r
-            });\r
-\r
-\r
-            gw.on('onuserlist_end', function (event) {\r
-                var channel;\r
-                channel = that.panels.getByName(event.channel);\r
-\r
-                // If we didn't find a channel for this, may aswell leave\r
-                if (!channel) return;\r
-\r
-                // Update the members list with the new list\r
-                channel.get('members').reset(channel.temp_userlist || []);\r
-\r
-                // Clear the temporary userlist\r
-                delete channel.temp_userlist;\r
-            });\r
-\r
-\r
-            gw.on('onmode', function (event) {\r
-                var channel, i, prefixes, members, member, find_prefix;\r
-                \r
-                // Build a nicely formatted string to be displayed to a regular human\r
-                function friendlyModeString (event_modes, alt_target) {\r
-                    var modes = {}, return_string;\r
-\r
-                    // If no default given, use the main event info\r
-                    if (!event_modes) {\r
-                        event_modes = event.modes;\r
-                        alt_target = event.target;\r
-                    }\r
-\r
-                    // Reformat the mode object to make it easier to work with\r
-                    _.each(event_modes, function (mode){\r
-                        var param = mode.param || alt_target || '';\r
-\r
-                        // Make sure we have some modes for this param\r
-                        if (!modes[param]) {\r
-                            modes[param] = {'+':'', '-':''};\r
-                        }\r
-\r
-                        modes[param][mode.mode[0]] += mode.mode.substr(1);\r
-                    });\r
-\r
-                    // Put the string together from each mode\r
-                    return_string = [];\r
-                    _.each(modes, function (modeset, param) {\r
-                        var str = '';\r
-                        if (modeset['+']) str += '+' + modeset['+'];\r
-                        if (modeset['-']) str += '-' + modeset['-'];\r
-                        return_string.push(str + ' ' + param);\r
-                    });\r
-                    return_string = return_string.join(', ');\r
-\r
-                    return return_string;\r
-                }\r
-\r
-\r
-                channel = that.panels.getByName(event.target);\r
-                if (channel) {\r
-                    prefixes = _kiwi.gateway.get('user_prefixes');\r
-                    find_prefix = function (p) {\r
-                        return event.modes[i].mode[1] === p.mode;\r
-                    };\r
-                    for (i = 0; i < event.modes.length; i++) {\r
-                        if (_.any(prefixes, find_prefix)) {\r
-                            if (!members) {\r
-                                members = channel.get('members');\r
-                            }\r
-                            member = members.getByNick(event.modes[i].param);\r
-                            if (!member) {\r
-                                console.log('MODE command recieved for unknown member %s on channel %s', event.modes[i].param, event.target);\r
-                                return;\r
-                            } else {\r
-                                if (event.modes[i].mode[0] === '+') {\r
-                                    member.addMode(event.modes[i].mode[1]);\r
-                                } else if (event.modes[i].mode[0] === '-') {\r
-                                    member.removeMode(event.modes[i].mode[1]);\r
-                                }\r
-                                members.sort();\r
-                                //channel.addMsg('', '== ' + event.nick + ' set mode ' + event.modes[i].mode + ' ' + event.modes[i].param, 'action mode');\r
-                            }\r
-                        } else {\r
-                            // Channel mode being set\r
-                            // TODO: Store this somewhere?\r
-                            //channel.addMsg('', 'CHANNEL === ' + event.nick + ' set mode ' + event.modes[i].mode + ' on ' + event.target, 'action mode');\r
-                        }\r
-                    }\r
-\r
-                    channel.addMsg('', '== ' + event.nick + ' sets mode ' + friendlyModeString(), 'action mode');\r
-                } else {\r
-                    // This is probably a mode being set on us.\r
-                    if (event.target.toLowerCase() === _kiwi.gateway.get("nick").toLowerCase()) {\r
-                        that.panels.server.addMsg('', '== ' + event.nick + ' set mode ' + friendlyModeString(), 'action mode');\r
-                    } else {\r
-                       console.log('MODE command recieved for unknown target %s: ', event.target, event);\r
-                    }\r
-                }\r
-            });\r
-\r
-\r
-            gw.on('onnick', function (event) {\r
-                var member;\r
-\r
-                $.each(that.panels.models, function (index, panel) {\r
-                    if (!panel.isChannel()) return;\r
-\r
-                    member = panel.get('members').getByNick(event.nick);\r
-                    if (member) {\r
-                        member.set('nick', event.newnick);\r
-                        panel.addMsg('', '== ' + event.nick + ' is now known as ' + event.newnick, 'action nick');\r
-                    }\r
-                });\r
-            });\r
-\r
-\r
-            gw.on('onwhois', function (event) {\r
-                /*globals secondsToTime */\r
-                var logon_date, idle_time = '', panel;\r
-\r
-                if (event.end) {\r
-                    return;\r
-                }\r
-\r
-                if (typeof event.idle !== 'undefined') {\r
-                    idle_time = secondsToTime(parseInt(event.idle, 10));\r
-                    idle_time = idle_time.h.toString().lpad(2, "0") + ':' + idle_time.m.toString().lpad(2, "0") + ':' + idle_time.s.toString().lpad(2, "0");\r
-                }\r
-\r
-                panel = _kiwi.app.panels.active;\r
-                if (event.ident) {\r
-                    panel.addMsg(event.nick, event.nick + ' [' + event.nick + '!' + event.ident + '@' + event.host + '] * ' + event.msg, 'whois');\r
-                } else if (event.chans) {\r
-                    panel.addMsg(event.nick, 'Channels: ' + event.chans, 'whois');\r
-                } else if (event.irc_server) {\r
-                    panel.addMsg(event.nick, 'Connected to server: ' + event.irc_server, 'whois');\r
-                } else if (event.msg) {\r
-                    panel.addMsg(event.nick, event.msg, 'whois');\r
-                } else if (event.logon) {\r
-                    logon_date = new Date();\r
-                    logon_date.setTime(event.logon * 1000);\r
-                    logon_date = formatDate(logon_date);\r
-\r
-                    panel.addMsg(event.nick, 'idle for ' + idle_time + ', signed on ' + logon_date, 'whois');\r
-                } else {\r
-                    panel.addMsg(event.nick, 'idle for ' + idle_time, 'whois');\r
-                }\r
-            });\r
-\r
-            gw.on('onaway', function (event) {\r
-                $.each(that.panels.models, function (index, panel) {\r
-                    if (!panel.isChannel()) return;\r
-\r
-                    member = panel.get('members').getByNick(event.nick);\r
-                    if (member) {\r
-                        member.set('away', !(!event.trailing));\r
-                    }\r
-                });\r
-            });\r
-\r
-\r
-            gw.on('onlist_start', function (data) {\r
-                var chanlist = _kiwi.model.Applet.loadOnce('kiwi_chanlist');\r
-                chanlist.view.show();\r
-            });\r
-\r
-\r
-            gw.on('onirc_error', function (data) {\r
-                var panel, tmp;\r
-\r
-                if (data.channel !== undefined && !(panel = _kiwi.app.panels.getByName(data.channel))) {\r
-                    panel = _kiwi.app.panels.server;\r
-                }\r
-\r
-                switch (data.error) {\r
-                case 'banned_from_channel':\r
-                    panel.addMsg(' ', '== You are banned from ' + data.channel + '. ' + data.reason, 'status');\r
-                    _kiwi.app.message.text('You are banned from ' + data.channel + '. ' + data.reason);\r
-                    break;\r
-                case 'bad_channel_key':\r
-                    panel.addMsg(' ', '== Bad channel key for ' + data.channel, 'status');\r
-                    _kiwi.app.message.text('Bad channel key or password for ' + data.channel);\r
-                    break;\r
-                case 'invite_only_channel':\r
-                    panel.addMsg(' ', '== ' + data.channel + ' is invite only.', 'status');\r
-                    _kiwi.app.message.text(data.channel + ' is invite only');\r
-                    break;\r
-                case 'channel_is_full':\r
-                    panel.addMsg(' ', '== ' + data.channel + ' is full.', 'status');\r
-                    _kiwi.app.message.text(data.channel + ' is full');\r
-                    break;\r
-                case 'chanop_privs_needed':\r
-                    panel.addMsg(' ', '== ' + data.reason, 'status');\r
-                    _kiwi.app.message.text(data.reason + ' (' + data.channel + ')');\r
-                    break;\r
-                case 'no_such_nick':\r
-                    tmp = _kiwi.app.panels.getByName(data.nick);\r
-                    if (tmp) {\r
-                        tmp.addMsg(' ', '== ' + data.nick + ': ' + data.reason, 'status');\r
-                    } else {\r
-                        _kiwi.app.panels.server.addMsg(' ', '== ' + data.nick + ': ' + data.reason, 'status');\r
-                    }\r
-                    break;\r
-                case 'nickname_in_use':\r
-                    _kiwi.app.panels.server.addMsg(' ', '== The nickname ' + data.nick + ' is already in use. Please select a new nickname', 'status');\r
-                    if (_kiwi.app.panels.server !== _kiwi.app.panels.active) {\r
-                        _kiwi.app.message.text('The nickname "' + data.nick + '" is already in use. Please select a new nickname');\r
-                    }\r
-\r
-                    // Only show the nickchange component if the controlbox is open\r
-                    if (that.controlbox.$el.css('display') !== 'none') {\r
-                        (new _kiwi.view.NickChangeBox()).render();\r
-                    }\r
-\r
-                case 'password_mismatch':\r
-                    _kiwi.app.panels.server.addMsg(' ', '== Incorrect password given', 'status');\r
-                    break;\r
-                default:\r
-                    // We don't know what data contains, so don't do anything with it.\r
-                    //_kiwi.front.tabviews.server.addMsg(null, ' ', '== ' + data, 'status');\r
-                }\r
-            });\r
         };\r
 \r
 \r
@@ -823,7 +412,7 @@ _kiwi.model.Application = function () {
             controlbox.on('command:part', partCommand);\r
 \r
             controlbox.on('command:nick', function (ev) {\r
-                _kiwi.gateway.changeNick(ev.params[0]);\r
+                _kiwi.gateway.changeNick(null, ev.params[0]);\r
             });\r
 \r
             controlbox.on('command:query', queryCommand);\r
@@ -840,6 +429,8 @@ _kiwi.model.Application = function () {
 \r
             controlbox.on('command:ctcp', ctcpCommand);\r
 \r
+            controlbox.on('command:server', serverCommand);\r
+\r
 \r
             controlbox.on('command:css', function (ev) {\r
                 var queryString = '?reload=' + new Date().getTime();\r
@@ -882,13 +473,13 @@ _kiwi.model.Application = function () {
                 }\r
 \r
                 // Read the value to the user\r
-                _kiwi.app.panels.active.addMsg('', setting + ' = ' + _kiwi.global.settings.get(setting).toString());\r
+                _kiwi.app.panels().active.addMsg('', setting + ' = ' + _kiwi.global.settings.get(setting).toString());\r
             });\r
 \r
 \r
             controlbox.on('command:save', function (ev) {\r
                 _kiwi.global.settings.save();\r
-                _kiwi.app.panels.active.addMsg('', 'Settings have been saved');\r
+                _kiwi.app.panels().active.addMsg('', 'Settings have been saved');\r
             });\r
 \r
 \r
@@ -898,7 +489,7 @@ _kiwi.model.Application = function () {
                 // No parameters passed so list them\r
                 if (!ev.params[1]) {\r
                     $.each(controlbox.preprocessor.aliases, function (name, rule) {\r
-                        _kiwi.app.panels.server.addMsg(' ', name + '   =>   ' + rule);\r
+                        _kiwi.app.panels().server.addMsg(' ', name + '   =>   ' + rule);\r
                     });\r
                     return;\r
                 }\r
@@ -930,12 +521,12 @@ _kiwi.model.Application = function () {
                 // No parameters passed so list them\r
                 if (!ev.params[0]) {\r
                     if (list.length > 0) {\r
-                        _kiwi.app.panels.active.addMsg(' ', 'Ignored nicks:');\r
+                        _kiwi.app.panels().active.addMsg(' ', 'Ignored nicks:');\r
                         $.each(list, function (idx, ignored_pattern) {\r
-                            _kiwi.app.panels.active.addMsg(' ', ignored_pattern);\r
+                            _kiwi.app.panels().active.addMsg(' ', ignored_pattern);\r
                         });\r
                     } else {\r
-                        _kiwi.app.panels.active.addMsg(' ', 'Not ignoring anybody');\r
+                        _kiwi.app.panels().active.addMsg(' ', 'Not ignoring anybody');\r
                     }\r
                     return;\r
                 }\r
@@ -943,7 +534,7 @@ _kiwi.model.Application = function () {
                 // We have a parameter, so add it\r
                 list.push(ev.params[0]);\r
                 _kiwi.gateway.set('ignore_list', list);\r
-                _kiwi.app.panels.active.addMsg(' ', 'Ignoring ' + ev.params[0]);\r
+                _kiwi.app.panels().active.addMsg(' ', 'Ignoring ' + ev.params[0]);\r
             });\r
 \r
 \r
@@ -951,7 +542,7 @@ _kiwi.model.Application = function () {
                 var list = _kiwi.gateway.get('ignore_list');\r
 \r
                 if (!ev.params[0]) {\r
-                    _kiwi.app.panels.active.addMsg(' ', 'Specifiy which nick you wish to stop ignoring');\r
+                    _kiwi.app.panels().active.addMsg(' ', 'Specifiy which nick you wish to stop ignoring');\r
                     return;\r
                 }\r
 \r
@@ -961,7 +552,7 @@ _kiwi.model.Application = function () {
 \r
                 _kiwi.gateway.set('ignore_list', list);\r
 \r
-                _kiwi.app.panels.active.addMsg(' ', 'Stopped ignoring ' + ev.params[0]);\r
+                _kiwi.app.panels().active.addMsg(' ', 'Stopped ignoring ' + ev.params[0]);\r
             });\r
 \r
 \r
@@ -974,44 +565,20 @@ _kiwi.model.Application = function () {
         function unknownCommand (ev) {\r
             var raw_cmd = ev.command + ' ' + ev.params.join(' ');\r
             console.log('RAW: ' + raw_cmd);\r
-            _kiwi.gateway.raw(raw_cmd);\r
+            _kiwi.gateway.raw(null, raw_cmd);\r
         }\r
 \r
         function allCommands (ev) {}\r
 \r
         function joinCommand (ev) {\r
-            var channel, channel_names;\r
+            var panels, channel_names;\r
 \r
             channel_names = ev.params.join(' ').split(',');\r
+            panels = that.connections.active_connection.createAndJoinChannels(channel_names);\r
 \r
-            $.each(channel_names, function (index, channel_name_key) {\r
-                // We may have a channel key so split it off\r
-                var spli = channel_name_key.split(' '),\r
-                    channel_name = spli[0],\r
-                    channel_key = spli[1] || '';\r
-\r
-                // Trim any whitespace off the name\r
-                channel_name = channel_name.trim();\r
-\r
-                // If not a valid channel name, display a warning\r
-                if (!that.isChannelName(channel_name)) {\r
-                    _kiwi.app.panels.server.addMsg('', channel_name + ' is not a valid channel name');\r
-                    _kiwi.app.message.text(channel_name + ' is not a valid channel name', {timeout: 5000});\r
-                    return;\r
-                }\r
-\r
-                // Check if we have the panel already. If not, create it\r
-                channel = that.panels.getByName(channel_name);\r
-                if (!channel) {\r
-                    channel = new _kiwi.model.Channel({name: channel_name});\r
-                    _kiwi.app.panels.add(channel);\r
-                }\r
-\r
-                _kiwi.gateway.join(channel_name, channel_key);\r
-            });\r
-\r
-            if (channel) channel.view.show();\r
-            \r
+            // Show the last channel if we have one\r
+            if (panels)\r
+                panels[panels.length - 1].view.show();\r
         }\r
 \r
         function queryCommand (ev) {\r
@@ -1020,10 +587,10 @@ _kiwi.model.Application = function () {
             destination = ev.params[0];\r
 \r
             // Check if we have the panel already. If not, create it\r
-            panel = that.panels.getByName(destination);\r
+            panel = that.connections.active_connection.panels.getByName(destination);\r
             if (!panel) {\r
                 panel = new _kiwi.model.Query({name: destination});\r
-                _kiwi.app.panels.add(panel);\r
+                that.connections.active_connection.panels.add(panel);\r
             }\r
 \r
             if (panel) panel.view.show();\r
@@ -1032,34 +599,32 @@ _kiwi.model.Application = function () {
 \r
         function msgCommand (ev) {\r
             var destination = ev.params[0],\r
-                panel = that.panels.getByName(destination) || that.panels.server;\r
+                panel = that.connections.active_connection.panels.getByName(destination) || that.panels().server;\r
 \r
             ev.params.shift();\r
 \r
-            panel.addMsg(_kiwi.gateway.get('nick'), ev.params.join(' '));\r
-            _kiwi.gateway.privmsg(destination, ev.params.join(' '));\r
+            panel.addMsg(_kiwi.app.connections.active_connection.get('nick'), ev.params.join(' '));\r
+            _kiwi.gateway.privmsg(null, destination, ev.params.join(' '));\r
         }\r
 \r
         function actionCommand (ev) {\r
-            if (_kiwi.app.panels.active === _kiwi.app.panels.server) {\r
+            if (_kiwi.app.panels().active.isServer()) {\r
                 return;\r
             }\r
 \r
-            var panel = _kiwi.app.panels.active;\r
-            panel.addMsg('', '* ' + _kiwi.gateway.get('nick') + ' ' + ev.params.join(' '), 'action');\r
-            _kiwi.gateway.action(panel.get('name'), ev.params.join(' '));\r
+            var panel = _kiwi.app.panels().active;\r
+            panel.addMsg('', '* ' + _kiwi.app.connections.active_connection.get('nick') + ' ' + ev.params.join(' '), 'action');\r
+            _kiwi.gateway.action(null, panel.get('name'), ev.params.join(' '));\r
         }\r
 \r
         function partCommand (ev) {\r
             if (ev.params.length === 0) {\r
-                _kiwi.gateway.part(_kiwi.app.panels.active.get('name'));\r
+                _kiwi.gateway.part(null, _kiwi.app.panels().active.get('name'));\r
             } else {\r
                 _.each(ev.params, function (channel) {\r
-                    _kiwi.gateway.part(channel);\r
+                    _kiwi.gateway.part(null, channel);\r
                 });\r
             }\r
-            // TODO: More responsive = close tab now, more accurate = leave until part event\r
-            //_kiwi.app.panels.remove(_kiwi.app.panels.active);\r
         }\r
 \r
         function topicCommand (ev) {\r
@@ -1071,10 +636,10 @@ _kiwi.model.Application = function () {
                 channel_name = ev.params[0];\r
                 ev.params.shift();\r
             } else {\r
-                channel_name = _kiwi.app.panels.active.get('name');\r
+                channel_name = _kiwi.app.panels().active.get('name');\r
             }\r
 \r
-            _kiwi.gateway.topic(channel_name, ev.params.join(' '));\r
+            _kiwi.gateway.topic(null, channel_name, ev.params.join(' '));\r
         }\r
 \r
         function noticeCommand (ev) {\r
@@ -1086,16 +651,16 @@ _kiwi.model.Application = function () {
             destination = ev.params[0];\r
             ev.params.shift();\r
 \r
-            _kiwi.gateway.notice(destination, ev.params.join(' '));\r
+            _kiwi.gateway.notice(null, destination, ev.params.join(' '));\r
         }\r
 \r
         function quoteCommand (ev) {\r
             var raw = ev.params.join(' ');\r
-            _kiwi.gateway.raw(raw);\r
+            _kiwi.gateway.raw(null, raw);\r
         }\r
 \r
         function kickCommand (ev) {\r
-            var nick, panel = _kiwi.app.panels.active;\r
+            var nick, panel = _kiwi.app.panels().active;\r
 \r
             if (!panel.isChannel()) return;\r
 \r
@@ -1105,17 +670,17 @@ _kiwi.model.Application = function () {
             nick = ev.params[0];\r
             ev.params.shift();\r
 \r
-            _kiwi.gateway.kick(panel.get('name'), nick, ev.params.join(' '));\r
+            _kiwi.gateway.kick(null, panel.get('name'), nick, ev.params.join(' '));\r
         }\r
 \r
         function clearCommand (ev) {\r
             // Can't clear a server or applet panel\r
-            if (_kiwi.app.panels.active.isServer() || _kiwi.app.panels.active.isApplet()) {\r
+            if (_kiwi.app.panels().active.isServer() || _kiwi.app.panels().active.isApplet()) {\r
                 return;\r
             }\r
 \r
-            if (_kiwi.app.panels.active.clearMessages) {\r
-                _kiwi.app.panels.active.clearMessages();\r
+            if (_kiwi.app.panels().active.clearMessages) {\r
+                _kiwi.app.panels().active.clearMessages();\r
             }\r
         }\r
 \r
@@ -1131,7 +696,7 @@ _kiwi.model.Application = function () {
             type = ev.params[0];\r
             ev.params.shift();\r
 \r
-            _kiwi.gateway.ctcp(true, type, target, ev.params.join(' '));\r
+            _kiwi.gateway.ctcp(null, true, type, target, ev.params.join(' '));\r
         }\r
 \r
         function settingsCommand (ev) {\r
@@ -1157,16 +722,80 @@ _kiwi.model.Application = function () {
                 if (_kiwi.applets[ev.params[0]]) {\r
                     panel.load(new _kiwi.applets[ev.params[0]]());\r
                 } else {\r
-                    _kiwi.app.panels.server.addMsg('', 'Applet "' + ev.params[0] + '" does not exist');\r
+                    _kiwi.app.panels().server.addMsg('', 'Applet "' + ev.params[0] + '" does not exist');\r
                     return;\r
                 }\r
             }\r
             \r
-            _kiwi.app.panels.add(panel);\r
+            _kiwi.app.connections.active_connection.panels.add(panel);\r
             panel.view.show();\r
         }\r
 \r
 \r
+        function serverCommand (ev) {\r
+            var server, port, ssl, password, nick,\r
+                tmp;\r
+\r
+            // If no server address given, show the new connection dialog\r
+            if (!ev.params[0]) {\r
+                tmp = new _kiwi.view.MenuBox('New Connection');\r
+                tmp.addItem('new_connection', new _kiwi.model.NewConnection().view.$el);\r
+                tmp.show();\r
+\r
+                // Center screen the dialog\r
+                tmp.$el.offset({\r
+                    top: (that.view.$el.height() / 2) - (tmp.$el.height() / 2),\r
+                    left: (that.view.$el.width() / 2) - (tmp.$el.width() / 2)\r
+                });\r
+\r
+                return;\r
+            }\r
+\r
+            // Port given in 'host:port' format and no specific port given after a space\r
+            if (ev.params[0].indexOf(':') > 0) {\r
+                tmp = ev.params[0].split(':');\r
+                server = tmp[0];\r
+                port = tmp[1];\r
+\r
+                password = ev.params[1] || undefined;\r
+\r
+            } else {\r
+                // Server + port given as 'host port'\r
+                server = ev.params[0];\r
+                port = ev.params[1] || 6667;\r
+\r
+                password = ev.params[2] || undefined;\r
+            }\r
+\r
+            // + in the port means SSL\r
+            if (port.toString()[0] === '+') {\r
+                ssl = true;\r
+                port = parseInt(port.substring(1), 10);\r
+            } else {\r
+                ssl = false;\r
+            }\r
+\r
+            // Default port if one wasn't found\r
+            port = port || 6667;\r
+\r
+            // Use the same nick as we currently have\r
+            nick = _kiwi.app.connections.active_connection.get('nick');\r
+\r
+            _kiwi.app.panels().active.addMsg('', 'Connecting to ' + server + ':' + port.toString() + '..');\r
+\r
+            _kiwi.gateway.newConnection({\r
+                nick: nick,\r
+                host: server,\r
+                port: port,\r
+                ssl: ssl,\r
+                password: password\r
+            }, function(err, new_connection) {\r
+                if (err)\r
+                    _kiwi.app.panels().active.addMsg('', 'Error connecting to ' + server + ':' + port.toString() + ' (' + err.toString() + ')');\r
+            });\r
+        }\r
+\r
+\r
 \r
 \r
 \r
@@ -1177,6 +806,7 @@ _kiwi.model.Application = function () {
             return (channel_prefix.indexOf(channel_name[0]) > -1);\r
         };\r
 \r
+\r
     };\r
 \r
 \r
index 19fd043401bd119707d778e9e3db26a6355c29f7..84b6ca715e5760393def473ebe0cba2319f4d8f6 100644 (file)
@@ -75,7 +75,8 @@ _kiwi.model.Gateway = function () {
         // Some easier handler events\r
         this.on('onmsg', function (event) {\r
             var source,\r
-                is_pm = (event.channel == that.get('nick'));\r
+                connection = _kiwi.app.connections.getByConnectionId(event.server),\r
+                is_pm = (event.channel == connection.get('nick'));\r
 \r
             source = is_pm ? event.nick : event.channel;\r
             \r
@@ -100,7 +101,8 @@ _kiwi.model.Gateway = function () {
 \r
         this.on('onaction', function (event) {\r
             var source,\r
-                is_pm = (event.channel == that.get('nick'));\r
+                connection = _kiwi.app.connections.getByConnectionId(event.server),\r
+                is_pm = (event.channel == connection.get('nick'));\r
 \r
             source = is_pm ? event.nick : event.channel;\r
             \r
@@ -130,13 +132,9 @@ _kiwi.model.Gateway = function () {
 \r
     /**\r
     *   Connects to the server\r
-    *   @param  {String}    host        The hostname or IP address of the IRC server to connect to\r
-    *   @param  {Number}    port        The port of the IRC server to connect to\r
-    *   @param  {Boolean}   ssl         Whether or not to connect to the IRC server using SSL\r
-    *   @param  {String}    password    The password to supply to the IRC server during registration\r
     *   @param  {Function}  callback    A callback function to be invoked once Kiwi's server has connected to the IRC server\r
     */\r
-    this.connect = function (host, port, ssl, password, callback) {\r
+    this.connect = function (callback) {\r
         var resource;\r
 \r
         // Work out the resource URL for socket.io\r
@@ -150,7 +148,7 @@ _kiwi.model.Gateway = function () {
 \r
         this.socket = io.connect(this.get('kiwi_server'), {\r
             'resource': resource,\r
-            \r
+\r
             'try multiple transports': true,\r
             'connect timeout': 3000,\r
             'max reconnection attempts': 7,\r
@@ -179,6 +177,8 @@ _kiwi.model.Gateway = function () {
          * IRCD and the nick has been accepted.\r
          */\r
         this.socket.on('connect', function () {\r
+            callback && callback();\r
+            /*\r
             this.emit('kiwi', {command: 'connect', nick: that.get('nick'), hostname: host, port: port, ssl: ssl, password:password}, function (err, server_num) {\r
                 if (!err) {\r
                     that.server_num = server_num;\r
@@ -188,6 +188,7 @@ _kiwi.model.Gateway = function () {
                     callback(err);\r
                 }\r
             });\r
+            */\r
         });\r
 \r
         this.socket.on('too_many_connections', function () {\r
@@ -223,6 +224,31 @@ _kiwi.model.Gateway = function () {
 \r
 \r
 \r
+    this.newConnection = function(connection_info, callback_fn) {\r
+        var that = this,\r
+            h = connection_info;\r
+\r
+        this.socket.emit('kiwi', {command: 'connect', nick: h.nick, hostname: h.host, port: h.port, ssl: h.ssl, password: h.password}, function (err, server_num) {\r
+            var connection;\r
+\r
+            if (!err) {\r
+                if (!_kiwi.app.connections.getByConnectionId(server_num)){\r
+                    connection = new _kiwi.model.Network({connection_id: server_num});\r
+                    _kiwi.app.connections.add(connection);\r
+                }\r
+\r
+                console.log("_kiwi.gateway.socket.on('connect')");\r
+                callback_fn && callback_fn(err, connection);\r
+                \r
+            } else {\r
+                console.log("_kiwi.gateway.socket.on('error')", {reason: err});\r
+                callback_fn && callback_fn(err);\r
+            }\r
+        });\r
+    };\r
+\r
+\r
+\r
     this.isConnected = function () {\r
         return this.socket.socket.connected;\r
     };\r
@@ -257,9 +283,8 @@ _kiwi.model.Gateway = function () {
     */\r
     this.parse = function (command, data) {\r
         //console.log('gateway event', command, data);\r
-        if (command !== undefined) {\r
-            that.trigger('on' + command, data);\r
 \r
+        if (command !== undefined) {\r
             switch (command) {\r
             case 'options':\r
                 $.each(data.options, function (name, value) {\r
@@ -278,15 +303,6 @@ _kiwi.model.Gateway = function () {
                 that.set('cap', data.cap);\r
                 break;\r
 \r
-            case 'connect':\r
-                that.set('nick', data.nick);\r
-                break;\r
-\r
-            case 'nick':\r
-                if (data.nick === that.get('nick')) {\r
-                    that.set('nick', data.newnick);\r
-                }\r
-                break;\r
             /*\r
             case 'sync':\r
                 if (_kiwi.gateway.onSync && _kiwi.gateway.syncing) {\r
@@ -301,6 +317,17 @@ _kiwi.model.Gateway = function () {
                 break;\r
             }\r
         }\r
+\r
+\r
+        if (typeof data.server !== 'undefined') {\r
+            that.trigger('connection:' + data.server.toString(), {\r
+                event_name: command,\r
+                event_data: data\r
+            });\r
+        }\r
+\r
+        // Trigger the global events (Mainly legacy now)\r
+        that.trigger('on' + command, data);\r
     };\r
 \r
     /**\r
@@ -309,8 +336,15 @@ _kiwi.model.Gateway = function () {
     *   @param  {Object}    data        The data to send\r
     *   @param  {Function}  callback    A callback function\r
     */\r
-    this.sendData = function (data, callback) {\r
-        this.socket.emit('irc', {server: 0, data: JSON.stringify(data)}, callback);\r
+    this.sendData = function (connection_id, data, callback) {\r
+        if (typeof connection_id === 'undefined' || connection_id === null)\r
+            connection_id = _kiwi.app.connections.active_connection.get('connection_id');\r
+\r
+        var data_buffer = {\r
+            server: connection_id,\r
+            data: JSON.stringify(data)\r
+        };\r
+        this.socket.emit('irc', data_buffer, callback);\r
     };\r
 \r
     /**\r
@@ -319,7 +353,7 @@ _kiwi.model.Gateway = function () {
     *   @param  {String}    msg         The message to send\r
     *   @param  {Function}  callback    A callback function\r
     */\r
-    this.privmsg = function (target, msg, callback) {\r
+    this.privmsg = function (connection_id, target, msg, callback) {\r
         var data = {\r
             method: 'privmsg',\r
             args: {\r
@@ -328,7 +362,7 @@ _kiwi.model.Gateway = function () {
             }\r
         };\r
 \r
-        this.sendData(data, callback);\r
+        this.sendData(connection_id, data, callback);\r
     };\r
 \r
     /**\r
@@ -337,7 +371,7 @@ _kiwi.model.Gateway = function () {
     *   @param  {String}    msg         The message to send\r
     *   @param  {Function}  callback    A callback function\r
     */\r
-    this.notice = function (target, msg, callback) {\r
+    this.notice = function (connection_id, target, msg, callback) {\r
         var data = {\r
             method: 'notice',\r
             args: {\r
@@ -346,7 +380,7 @@ _kiwi.model.Gateway = function () {
             }\r
         };\r
 \r
-        this.sendData(data, callback);\r
+        this.sendData(connection_id, data, callback);\r
     };\r
 \r
     /**\r
@@ -357,7 +391,7 @@ _kiwi.model.Gateway = function () {
     *   @param  {String}    params      Additional paramaters\r
     *   @param  {Function}  callback    A callback function\r
     */\r
-    this.ctcp = function (request, type, target, params, callback) {\r
+    this.ctcp = function (connection_id, request, type, target, params, callback) {\r
         var data = {\r
             method: 'ctcp',\r
             args: {\r
@@ -368,7 +402,7 @@ _kiwi.model.Gateway = function () {
             }\r
         };\r
 \r
-        this.sendData(data, callback);\r
+        this.sendData(connection_id, data, callback);\r
     };\r
 \r
     /**\r
@@ -376,7 +410,7 @@ _kiwi.model.Gateway = function () {
     *   @param  {String}    msg         The message to send\r
     *   @param  {Function}  callback    A callback function\r
     */\r
-    this.action = function (target, msg, callback) {\r
+    this.action = function (connection_id, target, msg, callback) {\r
         this.ctcp(true, 'ACTION', target, msg, callback);\r
     };\r
 \r
@@ -386,7 +420,7 @@ _kiwi.model.Gateway = function () {
     *   @param  {String}    key         The key to the channel\r
     *   @param  {Function}  callback    A callback function\r
     */\r
-    this.join = function (channel, key, callback) {\r
+    this.join = function (connection_id, channel, key, callback) {\r
         var data = {\r
             method: 'join',\r
             args: {\r
@@ -395,7 +429,7 @@ _kiwi.model.Gateway = function () {
             }\r
         };\r
 \r
-        this.sendData(data, callback);\r
+        this.sendData(connection_id, data, callback);\r
     };\r
 \r
     /**\r
@@ -403,7 +437,7 @@ _kiwi.model.Gateway = function () {
     *   @param  {String}    channel     The channel to part\r
     *   @param  {Function}  callback    A callback function\r
     */\r
-    this.part = function (channel, callback) {\r
+    this.part = function (connection_id, channel, callback) {\r
         var data = {\r
             method: 'part',\r
             args: {\r
@@ -411,7 +445,7 @@ _kiwi.model.Gateway = function () {
             }\r
         };\r
 \r
-        this.sendData(data, callback);\r
+        this.sendData(connection_id, data, callback);\r
     };\r
 \r
     /**\r
@@ -420,7 +454,7 @@ _kiwi.model.Gateway = function () {
     *   @param  {String}    new_topic   The new topic to set\r
     *   @param  {Function}  callback    A callback function\r
     */\r
-    this.topic = function (channel, new_topic, callback) {\r
+    this.topic = function (connection_id, channel, new_topic, callback) {\r
         var data = {\r
             method: 'topic',\r
             args: {\r
@@ -429,7 +463,7 @@ _kiwi.model.Gateway = function () {
             }\r
         };\r
 \r
-        this.sendData(data, callback);\r
+        this.sendData(connection_id, data, callback);\r
     };\r
 \r
     /**\r
@@ -439,7 +473,7 @@ _kiwi.model.Gateway = function () {
     *   @param  {String}    reason      The reason for kicking the user\r
     *   @param  {Function}  callback    A callback function\r
     */\r
-    this.kick = function (channel, nick, reason, callback) {\r
+    this.kick = function (connection_id, channel, nick, reason, callback) {\r
         var data = {\r
             method: 'kick',\r
             args: {\r
@@ -449,7 +483,7 @@ _kiwi.model.Gateway = function () {
             }\r
         };\r
 \r
-        this.sendData(data, callback);\r
+        this.sendData(connection_id, data, callback);\r
     };\r
 \r
     /**\r
@@ -457,7 +491,7 @@ _kiwi.model.Gateway = function () {
     *   @param  {String}    msg         The quit message to send to the IRC server\r
     *   @param  {Function}   callback    A callback function\r
     */\r
-    this.quit = function (msg, callback) {\r
+    this.quit = function (connection_id, msg, callback) {\r
         msg = msg || "";\r
         var data = {\r
             method: 'quit',\r
@@ -466,7 +500,7 @@ _kiwi.model.Gateway = function () {
             }\r
         };\r
 \r
-        this.sendData(data, callback);\r
+        this.sendData(connection_id, data, callback);\r
     };\r
 \r
     /**\r
@@ -474,7 +508,7 @@ _kiwi.model.Gateway = function () {
     *   @param  {String}    data        The data to send to the IRC server\r
     *   @param  {Function}  callback    A callback function\r
     */\r
-    this.raw = function (data, callback) {\r
+    this.raw = function (connection_id, data, callback) {\r
         data = {\r
             method: 'raw',\r
             args: {\r
@@ -482,7 +516,7 @@ _kiwi.model.Gateway = function () {
             }\r
         };\r
 \r
-        this.sendData(data, callback);\r
+        this.sendData(connection_id, data, callback);\r
     };\r
 \r
     /**\r
@@ -490,7 +524,7 @@ _kiwi.model.Gateway = function () {
     *   @param  {String}    new_nick    Our new nickname\r
     *   @param  {Function}  callback    A callback function\r
     */\r
-    this.changeNick = function (new_nick, callback) {\r
+    this.changeNick = function (connection_id, new_nick, callback) {\r
         var data = {\r
             method: 'nick',\r
             args: {\r
@@ -498,7 +532,7 @@ _kiwi.model.Gateway = function () {
             }\r
         };\r
 \r
-        this.sendData(data, callback);\r
+        this.sendData(connection_id, data, callback);\r
     };\r
 \r
     /**\r
@@ -535,7 +569,7 @@ _kiwi.model.Gateway = function () {
         }\r
 \r
         return false;\r
-    }\r
+    };\r
 \r
 \r
     return new (Backbone.Model.extend(this))(arguments);\r
diff --git a/client/assets/dev/model_network.js b/client/assets/dev/model_network.js
new file mode 100644 (file)
index 0000000..c21f5e8
--- /dev/null
@@ -0,0 +1,649 @@
+(function () {
+
+    _kiwi.model.Network = Backbone.Model.extend({
+        defaults: {
+            connection_id: 0,
+            /**
+            *   The name of the network
+            *   @type    String
+            */
+            name: 'Network',
+
+            /**
+            *   The address (URL) of the network
+            *   @type    String
+            */
+            address: '',
+
+            /**
+            *   The current nickname
+            *   @type   String
+            */
+            nick: '',
+
+            /**
+            *   The channel prefix for this network
+            *   @type    String
+            */
+            channel_prefix: '#',
+
+            /**
+            *   The user prefixes for channel owner/admin/op/voice etc. on this network
+            *   @type   Array
+            */
+            user_prefixes: ['~', '&', '@', '+']
+        },
+
+
+        initialize: function () {
+            this.gateway = _kiwi.global.components.Network(this.get('connection_id'));
+            this.bindGatewayEvents();
+
+            // Create our panel list (tabs)
+            this.panels = new _kiwi.model.PanelList([], this);
+            //this.panels.network = this;
+
+            // Automatically create a server tab
+            var server_panel = new _kiwi.model.Server({name: 'Server'});
+            this.panels.add(server_panel);
+            this.panels.server = this.panels.active = server_panel;
+        },
+
+
+        bindGatewayEvents: function () {
+            //this.gateway.on('all', function() {console.log('ALL', this.get('connection_id'), arguments);});
+
+            this.gateway.on('connect', onConnect, this);
+
+            this.gateway.on('nick', function(event) {
+                if (event.nick === this.get('nick')) {
+                    this.set('nick', event.newnick);
+                }
+            }, this);
+
+            this.gateway.on('options', onOptions, this);
+            this.gateway.on('motd', onMotd, this);
+            this.gateway.on('join', onJoin, this);
+            this.gateway.on('part', onPart, this);
+            this.gateway.on('quit', onQuit, this);
+            this.gateway.on('kick', onKick, this);
+            this.gateway.on('msg', onMsg, this);
+            this.gateway.on('nick', onNick, this);
+            this.gateway.on('ctcp_request', onCtcpRequest, this);
+            this.gateway.on('ctcp_response', onCtcpResponse, this);
+            this.gateway.on('notice', onNotice, this);
+            this.gateway.on('action', onAction, this);
+            this.gateway.on('topic', onTopic, this);
+            this.gateway.on('topicsetby', onTopicSetBy, this);
+            this.gateway.on('userlist', onUserlist, this);
+            this.gateway.on('userlist_end', onUserlistEnd, this);
+            this.gateway.on('mode', onMode, this);
+            this.gateway.on('whois', onWhois, this);
+            this.gateway.on('away', onAway, this);
+            this.gateway.on('list_start', onListStart, this);
+        },
+
+
+        /**
+         * Create panels and join the channel
+         * This will not wait for the join event to create a panel. This
+         * increases responsiveness in case of network lag
+         */
+        createAndJoinChannels: function (channels) {
+            var that = this,
+                panels = [];
+
+            // Multiple channels may come as comma-delimited 
+            if (typeof channels === 'string') {
+                channels = channels.split(',');
+            }
+
+            $.each(channels, function (index, channel_name_key) {
+                // We may have a channel key so split it off
+                var spli = channel_name_key.trim().split(' '),
+                    channel_name = spli[0],
+                    channel_key = spli[1] || '';
+
+                // Trim any whitespace off the name
+                channel_name = channel_name.trim();
+
+                // If not a valid channel name, display a warning
+                if (!_kiwi.app.isChannelName(channel_name)) {
+                    that.panels.server.addMsg('', channel_name + ' is not a valid channel name');
+                    _kiwi.app.message.text(channel_name + ' is not a valid channel name', {timeout: 5000});
+                    return;
+                }
+
+                // Check if we have the panel already. If not, create it
+                channel = that.panels.getByName(channel_name);
+                if (!channel) {
+                    channel = new _kiwi.model.Channel({name: channel_name});
+                    that.panels.add(channel);
+                }
+
+                panels.push(channel);
+
+                that.gateway.join(channel_name, channel_key);
+            });
+
+            return panels;
+        }
+    });
+
+
+
+    function onConnect(event) {
+        var panels, channel_names;
+
+        // Update our nick with what the network gave us
+        this.set('nick', event.nick);
+
+        // Auto joining channels
+        if (this.auto_join && this.auto_join.channel) {
+            panels = this.createAndJoinChannels(this.auto_join.channel + ' ' + (this.auto_join.channel_key || ''));
+
+            // Show the last channel if we have one
+            if (panels)
+                panels[panels.length - 1].view.show();
+        }
+    }
+
+
+
+    function onOptions(event) {
+        var that = this;
+
+        $.each(event.options, function (name, value) {
+            switch (name) {
+            case 'CHANTYPES':
+                that.set('channel_prefix', value.join(''));
+                break;
+            case 'NETWORK':
+                that.set('name', value);
+                break;
+            case 'PREFIX':
+                that.set('user_prefixes', value);
+                break;
+            }
+        });
+
+        this.set('cap', event.cap);
+    }
+
+
+
+    function onMotd(event) {
+        this.panels.server.addMsg(this.get('name'), event.msg, 'motd');
+    }
+
+
+
+    function onJoin(event) {
+        var c, members, user;
+        c = this.panels.getByName(event.channel);
+        if (!c) {
+            c = new _kiwi.model.Channel({name: event.channel});
+            this.panels.add(c);
+        }
+
+        members = c.get('members');
+        if (!members) return;
+
+        user = new _kiwi.model.Member({nick: event.nick, ident: event.ident, hostname: event.hostname});
+        members.add(user);
+    }
+
+
+
+    function onPart(event) {
+        var channel, members, user,
+            part_options = {};
+
+        part_options.type = 'part';
+        part_options.message = event.message || '';
+
+        channel = this.panels.getByName(event.channel);
+        if (!channel) return;
+
+        // If this is us, close the panel
+        if (event.nick === this.get('nick')) {
+            channel.close();
+            return;
+        }
+
+        members = channel.get('members');
+        if (!members) return;
+
+        user = members.getByNick(event.nick);
+        if (!user) return;
+
+        members.remove(user, part_options);
+    }
+
+
+
+    function onQuit(event) {
+        var member, members,
+            quit_options = {};
+
+        quit_options.type = 'quit';
+        quit_options.message = event.message || '';
+
+        $.each(this.panels.models, function (index, panel) {
+            if (!panel.isChannel()) return;
+
+            member = panel.get('members').getByNick(event.nick);
+            if (member) {
+                panel.get('members').remove(member, quit_options);
+            }
+        });
+    }
+
+
+
+    function onKick(event) {
+        var channel, members, user,
+            part_options = {};
+
+        part_options.type = 'kick';
+        part_options.by = event.nick;
+        part_options.message = event.message || '';
+
+        channel = this.panels.getByName(event.channel);
+        if (!channel) return;
+
+        members = channel.get('members');
+        if (!members) return;
+
+        user = members.getByNick(event.kicked);
+        if (!user) return;
+
+        members.remove(user, part_options);
+
+        if (event.kicked === this.get('nick')) {
+            members.reset([]);
+        }
+    }
+
+
+
+    function onMsg(event) {
+        var panel,
+            is_pm = (event.channel == this.get('nick'));
+
+        // An ignored user? don't do anything with it
+        if (_kiwi.gateway.isNickIgnored(event.nick)) {
+            return;
+        }
+
+        if (is_pm) {
+            // If a panel isn't found for this PM, create one
+            panel = this.panels.getByName(event.nick);
+            if (!panel) {
+                panel = new _kiwi.model.Query({name: event.nick});
+                this.panels.add(panel);
+            }
+
+        } else {
+            // If a panel isn't found for this channel, reroute to the
+            // server panel
+            panel = this.panels.getByName(event.channel);
+            if (!panel) {
+                panel = this.panels.server;
+            }
+        }
+
+        panel.addMsg(event.nick, event.msg);
+    }
+
+
+
+    function onNick(event) {
+        var member;
+
+        $.each(this.panels.models, function (index, panel) {
+            if (panel.get('name') == event.nick)
+                panel.set('name', event.newnick);
+
+            if (!panel.isChannel()) return;
+
+            member = panel.get('members').getByNick(event.nick);
+            if (member) {
+                member.set('nick', event.newnick);
+                panel.addMsg('', '== ' + event.nick + ' is now known as ' + event.newnick, 'action nick');
+            }
+        });
+    }
+
+
+
+    function onCtcpRequest(event) {
+        // An ignored user? don't do anything with it
+        if (_kiwi.gateway.isNickIgnored(event.nick)) {
+            return;
+        }
+
+        // Reply to a TIME ctcp
+        if (event.msg.toUpperCase() === 'TIME') {
+            this.gateway.ctcp(null, false, event.type, event.nick, (new Date()).toString());
+        }
+    }
+
+
+
+    function onCtcpResponse(event) {
+        // An ignored user? don't do anything with it
+        if (_kiwi.gateway.isNickIgnored(event.nick)) {
+            return;
+        }
+
+        this.panels.server.addMsg('[' + event.nick + ']', 'CTCP ' + event.msg);
+    }
+
+
+
+    function onNotice(event) {
+        var panel;
+
+        // An ignored user? don't do anything with it
+        if (event.nick && _kiwi.gateway.isNickIgnored(event.nick)) {
+            return;
+        }
+
+        // Find a panel for the destination(channel) or who its from
+        panel = this.panels.getByName(event.target) || this.panels.getByName(event.nick);
+        if (!panel) {
+            panel = this.panels.server;
+        }
+
+        panel.addMsg('[' + (event.nick||'') + ']', event.msg);
+    }
+
+
+
+    function onAction(event) {
+        var panel,
+            is_pm = (event.channel == this.get('nick'));
+
+        // An ignored user? don't do anything with it
+        if (_kiwi.gateway.isNickIgnored(event.nick)) {
+            return;
+        }
+
+        if (is_pm) {
+            // If a panel isn't found for this PM, create one
+            panel = this.panels.getByName(event.nick);
+            if (!panel) {
+                panel = new _kiwi.model.Channel({name: event.nick});
+                this.panels.add(panel);
+            }
+
+        } else {
+            // If a panel isn't found for this channel, reroute to the
+            // server panel
+            panel = this.panels.getByName(event.channel);
+            if (!panel) {
+                panel = this.panels.server;
+            }
+        }
+
+        panel.addMsg('', '* ' + event.nick + ' ' + event.msg, 'action');
+    }
+
+
+
+    function onTopic(event) {
+        var c;
+        c = this.panels.getByName(event.channel);
+        if (!c) return;
+
+        // Set the channels topic
+        c.set('topic', event.topic);
+
+        // If this is the active channel, update the topic bar too
+        if (c.get('name') === this.panels.active.get('name')) {
+            _kiwi.app.topicbar.setCurrentTopic(event.topic);
+        }
+    }
+
+
+
+    function onTopicSetBy(event) {
+        var c, when;
+        c = this.panels.getByName(event.channel);
+        if (!c) return;
+
+        when = formatDate(new Date(event.when * 1000));
+        c.addMsg('', 'Topic set by ' + event.nick + ' at ' + when, 'topic');
+    }
+
+
+
+    function onUserlist(event) {
+        var channel;
+        channel = this.panels.getByName(event.channel);
+
+        // If we didn't find a channel for this, may aswell leave
+        if (!channel) return;
+
+        channel.temp_userlist = channel.temp_userlist || [];
+        _.each(event.users, function (item) {
+            var user = new _kiwi.model.Member({nick: item.nick, modes: item.modes});
+            channel.temp_userlist.push(user);
+        });
+    }
+
+
+
+    function onUserlistEnd(event) {
+        var channel;
+        channel = this.panels.getByName(event.channel);
+
+        // If we didn't find a channel for this, may aswell leave
+        if (!channel) return;
+
+        // Update the members list with the new list
+        channel.get('members').reset(channel.temp_userlist || []);
+
+        // Clear the temporary userlist
+        delete channel.temp_userlist;
+    }
+
+
+
+    function onMode(event) {
+        var channel, i, prefixes, members, member, find_prefix;
+
+        // Build a nicely formatted string to be displayed to a regular human
+        function friendlyModeString (event_modes, alt_target) {
+            var modes = {}, return_string;
+
+            // If no default given, use the main event info
+            if (!event_modes) {
+                event_modes = event.modes;
+                alt_target = event.target;
+            }
+
+            // Reformat the mode object to make it easier to work with
+            _.each(event_modes, function (mode){
+                var param = mode.param || alt_target || '';
+
+                // Make sure we have some modes for this param
+                if (!modes[param]) {
+                    modes[param] = {'+':'', '-':''};
+                }
+
+                modes[param][mode.mode[0]] += mode.mode.substr(1);
+            });
+
+            // Put the string together from each mode
+            return_string = [];
+            _.each(modes, function (modeset, param) {
+                var str = '';
+                if (modeset['+']) str += '+' + modeset['+'];
+                if (modeset['-']) str += '-' + modeset['-'];
+                return_string.push(str + ' ' + param);
+            });
+            return_string = return_string.join(', ');
+
+            return return_string;
+        }
+
+
+        channel = this.panels.getByName(event.target);
+        if (channel) {
+            prefixes = this.get('user_prefixes');
+            find_prefix = function (p) {
+                return event.modes[i].mode[1] === p.mode;
+            };
+            for (i = 0; i < event.modes.length; i++) {
+                if (_.any(prefixes, find_prefix)) {
+                    if (!members) {
+                        members = channel.get('members');
+                    }
+                    member = members.getByNick(event.modes[i].param);
+                    if (!member) {
+                        console.log('MODE command recieved for unknown member %s on channel %s', event.modes[i].param, event.target);
+                        return;
+                    } else {
+                        if (event.modes[i].mode[0] === '+') {
+                            member.addMode(event.modes[i].mode[1]);
+                        } else if (event.modes[i].mode[0] === '-') {
+                            member.removeMode(event.modes[i].mode[1]);
+                        }
+                        members.sort();
+                        //channel.addMsg('', '== ' + event.nick + ' set mode ' + event.modes[i].mode + ' ' + event.modes[i].param, 'action mode');
+                    }
+                } else {
+                    // Channel mode being set
+                    // TODO: Store this somewhere?
+                    //channel.addMsg('', 'CHANNEL === ' + event.nick + ' set mode ' + event.modes[i].mode + ' on ' + event.target, 'action mode');
+                }
+            }
+
+            channel.addMsg('', '== ' + event.nick + ' sets mode ' + friendlyModeString(), 'action mode');
+        } else {
+            // This is probably a mode being set on us.
+            if (event.target.toLowerCase() === this.get("nick").toLowerCase()) {
+                this.panels.server.addMsg('', '== ' + event.nick + ' set mode ' + friendlyModeString(), 'action mode');
+            } else {
+               console.log('MODE command recieved for unknown target %s: ', event.target, event);
+            }
+        }
+    }
+
+
+
+    function onWhois(event) {
+        var logon_date, idle_time = '', panel;
+
+        if (event.end)
+            return;
+
+        if (typeof event.idle !== 'undefined') {
+            idle_time = secondsToTime(parseInt(event.idle, 10));
+            idle_time = idle_time.h.toString().lpad(2, "0") + ':' + idle_time.m.toString().lpad(2, "0") + ':' + idle_time.s.toString().lpad(2, "0");
+        }
+
+        panel = _kiwi.app.panels().active;
+        if (event.ident) {
+            panel.addMsg(event.nick, event.nick + ' [' + event.nick + '!' + event.ident + '@' + event.host + '] * ' + event.msg, 'whois');
+        } else if (event.chans) {
+            panel.addMsg(event.nick, 'Channels: ' + event.chans, 'whois');
+        } else if (event.irc_server) {
+            panel.addMsg(event.nick, 'Connected to server: ' + event.irc_server, 'whois');
+        } else if (event.msg) {
+            panel.addMsg(event.nick, event.msg, 'whois');
+        } else if (event.logon) {
+            logon_date = new Date();
+            logon_date.setTime(event.logon * 1000);
+            logon_date = formatDate(logon_date);
+
+            panel.addMsg(event.nick, 'idle for ' + idle_time + ', signed on ' + logon_date, 'whois');
+        } else {
+            panel.addMsg(event.nick, 'idle for ' + idle_time, 'whois');
+        }
+    }
+
+
+
+    function onAway(event) {
+        $.each(this.panels.models, function (index, panel) {
+            if (!panel.isChannel()) return;
+
+            member = panel.get('members').getByNick(event.nick);
+            if (member) {
+                member.set('away', !(!event.trailing));
+            }
+        });
+    }
+
+
+
+    function onListStart(event) {
+        var chanlist = _kiwi.model.Applet.loadOnce('kiwi_chanlist');
+        chanlist.view.show();
+    }
+
+
+
+    function onIrcError(event) {
+        var panel, tmp;
+
+        if (event.channel !== undefined && !(panel = _kiwi.app.panels.getByName(event.channel))) {
+            panel = this.panels.server;
+        }
+
+        switch (event.error) {
+        case 'banned_from_channel':
+            panel.addMsg(' ', '== You are banned from ' + event.channel + '. ' + event.reason, 'status');
+            _kiwi.app.message.text('You are banned from ' + event.channel + '. ' + event.reason);
+            break;
+        case 'bad_channel_key':
+            panel.addMsg(' ', '== Bad channel key for ' + event.channel, 'status');
+            _kiwi.app.message.text('Bad channel key or password for ' + event.channel);
+            break;
+        case 'invite_only_channel':
+            panel.addMsg(' ', '== ' + event.channel + ' is invite only.', 'status');
+            _kiwi.app.message.text(event.channel + ' is invite only');
+            break;
+        case 'channel_is_full':
+            panel.addMsg(' ', '== ' + event.channel + ' is full.', 'status');
+            _kiwi.app.message.text(event.channel + ' is full');
+            break;
+        case 'chanop_privs_needed':
+            panel.addMsg(' ', '== ' + event.reason, 'status');
+            _kiwi.app.message.text(event.reason + ' (' + event.channel + ')');
+            break;
+        case 'no_such_nick':
+            tmp = this.panels.getByName(event.nick);
+            if (tmp) {
+                tmp.addMsg(' ', '== ' + event.nick + ': ' + event.reason, 'status');
+            } else {
+                this.panels.server.addMsg(' ', '== ' + event.nick + ': ' + event.reason, 'status');
+            }
+            break;
+        case 'nickname_in_use':
+            this.panels.server.addMsg(' ', '== The nickname ' + event.nick + ' is already in use. Please select a new nickname', 'status');
+            if (this.panels.server !== thia.panels.active) {
+                _kiwi.app.message.text('The nickname "' + event.nick + '" is already in use. Please select a new nickname');
+            }
+
+            // Only show the nickchange component if the controlbox is open
+            if (that.controlbox.$el.css('display') !== 'none') {
+                (new _kiwi.view.NickChangeBox()).render();
+            }
+
+            break;
+
+        case 'password_mismatch':
+            this.panels.server.addMsg(' ', '== Incorrect password given', 'status');
+            break;
+        default:
+            // We don't know what data contains, so don't do anything with it.
+            //_kiwi.front.tabviews.server.addMsg(null, ' ', '== ' + data, 'status');
+        }
+    }
+}
+
+)();
\ No newline at end of file
diff --git a/client/assets/dev/model_networkpanellist.js b/client/assets/dev/model_networkpanellist.js
new file mode 100644 (file)
index 0000000..96785c3
--- /dev/null
@@ -0,0 +1,62 @@
+_kiwi.model.NetworkPanelList = Backbone.Collection.extend({
+    model: _kiwi.model.Network,
+
+    initialize: function() {
+        this.view = new _kiwi.view.NetworkTabs({model: this});
+        
+        this.on('add', this.onNetworkAdd, this);
+        this.on('remove', this.onNetworkRemove, this);
+
+        // Current active connection / panel
+        this.active_connection = undefined;
+        this.active_panel = undefined;
+
+        // TODO: Remove this - legacy
+        this.active = undefined;
+    },
+
+    getByConnectionId: function(id) {
+        return this.find(function(connection){
+            return connection.get('connection_id') == id;
+        });
+    },
+
+    panels: function() {
+        var panels = [];
+
+        this.each(function(network) {
+            panels = panels.concat(network.panels.models);
+        });
+
+        return panels;
+    },
+
+
+    onNetworkAdd: function(network) {
+        network.panels.on('active', this.onPanelActive, this);
+
+        // if it's our first connection, set it active
+        if (this.models.length === 1) {
+            this.active_connection = network;
+            this.active_panel = network.panels.server;
+
+            // TODO: Remove this - legacy
+            this.active = this.active_panel;
+        }
+    },
+
+    onNetworkRemove: function(network) {
+        network.panels.off('active', this.onPanelActive, this);
+    },
+
+    onPanelActive: function(panel) {
+        var connection = this.getByConnectionId(panel.tab.data('connection_id'));
+        this.trigger('active', panel, connection);
+
+        this.active_connection = connection;
+        this.active_panel = panel;
+
+        // TODO: Remove this - legacy
+        this.active = panel;
+    }
+});
\ No newline at end of file
diff --git a/client/assets/dev/model_newconnection.js b/client/assets/dev/model_newconnection.js
new file mode 100644 (file)
index 0000000..368f8bf
--- /dev/null
@@ -0,0 +1,79 @@
+_kiwi.model.NewConnection = Backbone.Collection.extend({
+    initialize: function() {
+        this.view = new _kiwi.view.ServerSelect();
+
+        this.view.bind('server_connect', this.onMakeConnection, this);
+
+    },
+
+
+    onMakeConnection: function(new_connection_event) {
+        var that = this,
+            transport_path = '',
+            auto_connect_details = new_connection_event;
+
+        this.view.networkConnecting();
+
+        
+        // If we don't have socket.io loaded, load it before opening a new connection
+        if (!window.io) {
+            // Path to get the socket.io transport code
+            transport_path = _kiwi.app.kiwi_server + _kiwi.app.get('base_path') + '/transport/socket.io.js?ts='+(new Date().getTime());
+                        
+            $script(transport_path, function() {
+                if (!window.io) {
+                    that.onKiwiServerNotFound();
+                    return;
+                }
+
+                _kiwi.gateway.set('kiwi_server', _kiwi.app.kiwi_server + '/kiwi');
+                _kiwi.gateway.connect(function() {
+                    that.makeConnection(new_connection_event);
+                });
+            });
+
+        } else {
+            this.makeConnection(new_connection_event);
+
+        }
+
+    },
+
+
+    onKiwiServerNotFound: function() {
+        this.view.showError();
+    },
+
+
+    makeConnection: function(new_connection_event) {
+        var that = this;
+
+        this.connect_details = new_connection_event;
+
+        _kiwi.gateway.newConnection({
+            nick: new_connection_event.nick,
+            host: new_connection_event.server,
+            port: new_connection_event.port,
+            ssl: new_connection_event.ssl,
+            password: new_connection_event.password
+        }, function(err, network) {
+            that.onNewNetwork(err, network);
+        });
+    },
+
+
+    onNewNetwork: function(err, network) {
+        if (network && this.connect_details) {
+            network.auto_join = {
+                channel: this.connect_details.channel,
+                key: this.connect_details.channel_key
+            };
+        }
+
+
+        // Show the server panel if this is our first network
+        if (network && network.get('connection_id') === 0) {
+            network.panels.server.view.show();
+        }
+    }
+});
\ No newline at end of file
index 158052ecb5aa942b3e6ad6530a8d900237003124..2efbe5aaeeadbce762cd7cc5844890d5afec1fd3 100644 (file)
@@ -79,14 +79,14 @@ _kiwi.model.Panel = Backbone.Model.extend({
             this.unset('members');\r
         }\r
 \r
-        _kiwi.app.panels.remove(this);\r
+        this.get('panel_list').remove(this);\r
 \r
         this.unbind();\r
         this.destroy();\r
 \r
         // If closing the active panel, switch to the server panel\r
-        if (this.cid === _kiwi.app.panels.active.cid) {\r
-            _kiwi.app.panels.server.view.show();\r
+        if (this === _kiwi.app.panels().active) {\r
+            _kiwi.app.connections.active_connection.panels.server.view.show();\r
         }\r
     },\r
 \r
@@ -120,6 +120,6 @@ _kiwi.model.Panel = Backbone.Model.extend({
     },\r
 \r
     isActive: function () {\r
-        return (_kiwi.app.panels.active === this);\r
+        return (_kiwi.app.panels().active === this);\r
     }\r
 });
\ No newline at end of file
index d95d58f54f5e80a9af7a9be8114a4fdd71ce1eb7..220f142d2021c7ba2d4138ce7f947c87c8cab930 100644 (file)
@@ -2,14 +2,17 @@ _kiwi.model.PanelList = Backbone.Collection.extend({
     model: _kiwi.model.Panel,\r
 \r
     comparator: function (chan) {\r
-        return chan.get("name");\r
+        return chan.get('name');\r
     },\r
-    initialize: function () {\r
-        this.view = new _kiwi.view.Tabs({"el": $('#tabs')[0], "model": this});\r
+    initialize: function (elements, network) {\r
+        var that = this;\r
 \r
-        // Automatically create a server tab\r
-        this.add(new _kiwi.model.Server({'name': _kiwi.gateway.get('name')}));\r
-        this.server = this.getByName(_kiwi.gateway.get('name'));\r
+        // If this PanelList is associated with a network/connection\r
+        if (network) {\r
+            this.network = network;\r
+        }\r
+\r
+        this.view = new _kiwi.view.Tabs({model: this});\r
 \r
         // Holds the active panel\r
         this.active = null;\r
@@ -19,11 +22,28 @@ _kiwi.model.PanelList = Backbone.Collection.extend({
             this.active = active_panel;\r
         }, this);\r
 \r
+        this.bind('add', function(panel) {\r
+            panel.set('panel_list', this);\r
+        });\r
+    },\r
+\r
+\r
+\r
+    getByCid: function (cid) {\r
+        if (typeof name !== 'string') return;\r
+\r
+        return this.find(function (c) {\r
+            return cid === c.cid;\r
+        });\r
     },\r
+\r
+\r
+\r
     getByName: function (name) {\r
         if (typeof name !== 'string') return;\r
+\r
         return this.find(function (c) {\r
             return name.toLowerCase() === c.get('name').toLowerCase();\r
         });\r
     }\r
-});
\ No newline at end of file
+});\r
index 196ab7d58dc29236020ddd4a2ece9eda9a1aafc0..2f7ddeeae1a7c47e49cbaba39a8ae34b35240a63 100644 (file)
@@ -11,10 +11,5 @@ _kiwi.model.Server = _kiwi.model.Panel.extend({
         }, {"silent": true});\r
 \r
         //this.addMsg(' ', '--> Kiwi IRC: Such an awesome IRC client', '', {style: 'color:#009900;'});\r
-\r
-        this.server_login = new _kiwi.view.ServerSelect();\r
-        \r
-        this.view.$el.append(this.server_login.$el);\r
-        this.server_login.show();\r
     }\r
 });
\ No newline at end of file
index 9dc7e369253d263c359d89dc8957b24b79c12ce5..f35803f9ef1a14fe7224010c2ec16592bde40c56 100644 (file)
@@ -285,7 +285,7 @@ function formatIRCMsg (msg) {
                     colours = openTags.colour.split(',');
                     style += 'color: ' + colours[0] + ((colours[1]) ? '; background-color: ' + colours[1] + ';' : '');
                 }
-                return '<span style="' + style + '">';
+                return '<span class="format_span" style="' + style + '">';
             }
         },
         colourMatch = function (str) {
index df3ca247312856484cb00f0f04b9631a25455446..7d60728fbf265bda31b3b76d60727b94e971dd43 100644 (file)
@@ -20,11 +20,13 @@ _kiwi.view.MemberList = Backbone.View.extend({
                 .data('member', member);\r
         });\r
     },\r
-    nickClick: function (x) {\r
-        var $target = $(x.currentTarget).parent('li'),\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
@@ -35,13 +37,31 @@ _kiwi.view.MemberList = Backbone.View.extend({
         userbox.member = member;\r
         userbox.channel = this.model.channel;\r
 \r
-        // Remove any existing userboxes\r
-        $('.userbox', this.$el).remove();\r
-\r
-        if (!this.model.getByNick(_kiwi.gateway.get('nick')).get('is_op')) {\r
+        if (!this.model.getByNick(_kiwi.app.connections.active_connection.get('nick')).get('is_op')) {\r
             userbox.$el.children('.if_op').remove();\r
         }\r
-        $target.append(userbox.$el);\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
         $('#memberlists').children().removeClass('active');\r
@@ -70,7 +90,7 @@ _kiwi.view.UserBox = Backbone.View.extend({
 \r
     queryClick: function (event) {\r
         var panel = new _kiwi.model.Query({name: this.member.get('nick')});\r
-        _kiwi.app.panels.add(panel);\r
+        _kiwi.app.connections.active_connection.panels.add(panel);\r
         panel.view.show();\r
     },\r
 \r
@@ -134,7 +154,10 @@ _kiwi.view.NickChangeBox = Backbone.View.extend({
 \r
     changeNick: function (event) {\r
         var that = this;\r
-        _kiwi.gateway.changeNick(this.$el.find('input').val(), function (err, val) {\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
@@ -149,7 +172,9 @@ _kiwi.view.ServerSelect = function () {
         events: {\r
             'submit form': 'submitForm',\r
             'click .show_more': 'showMore',\r
-            'change .have_pass input': 'showPass'\r
+            'change .have_pass input': 'showPass',\r
+            'change .have_key input': 'showKey',\r
+            'click .icon-key': 'channelKeyIconClick'\r
         },\r
 \r
         initialize: function () {\r
@@ -165,24 +190,17 @@ _kiwi.view.ServerSelect = function () {
                 }\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
-            _kiwi.gateway.bind('onirc_error', function (data) {\r
-                $('button', this.$el).attr('disabled', null);\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
-                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
-            }, this);\r
+            this.$el.remove();\r
         },\r
 \r
         submitForm: function (event) {\r
@@ -208,7 +226,7 @@ _kiwi.view.ServerSelect = function () {
         submitLogin: function (event) {\r
             // If submitting is disabled, don't do anything\r
             if ($('button', this.$el).attr('disabled')) return;\r
-            \r
+\r
             var values = {\r
                 nick: $('input.nick', this.$el).val(),\r
                 server: $('input.server', this.$el).val(),\r
@@ -223,7 +241,7 @@ _kiwi.view.ServerSelect = function () {
         },\r
 \r
         submitNickChange: function (event) {\r
-            _kiwi.gateway.changeNick($('input.nick', this.$el).val());\r
+            _kiwi.gateway.changeNick(null, $('input.nick', this.$el).val());\r
             this.networkConnecting();\r
         },\r
 \r
@@ -235,6 +253,18 @@ _kiwi.view.ServerSelect = function () {
             }\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
@@ -257,9 +287,17 @@ _kiwi.view.ServerSelect = function () {
             $('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
@@ -286,6 +324,26 @@ _kiwi.view.ServerSelect = function () {
             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
@@ -306,6 +364,21 @@ _kiwi.view.ServerSelect = function () {
             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 (event) {\r
             this.setStatus('Error connecting', 'error');\r
             $('button', this.$el).attr('disabled', null);\r
@@ -320,7 +393,8 @@ _kiwi.view.ServerSelect = function () {
 \r
 _kiwi.view.Panel = Backbone.View.extend({\r
     tagName: "div",\r
-    className: "messages",\r
+    className: "panel messages",\r
+\r
     events: {\r
         "click .chan": "chanClick",\r
         'click .media .open': 'mediaClick',\r
@@ -367,7 +441,7 @@ _kiwi.view.Panel = Backbone.View.extend({
             nick_colour_hex, nick_hex, is_highlight, msg_css_classes = '';\r
 \r
         // Nick highlight detecting\r
-        if ((new RegExp('\\b' + _kiwi.gateway.get('nick') + '\\b', 'i')).test(msg.msg)) {\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
@@ -383,7 +457,7 @@ _kiwi.view.Panel = Backbone.View.extend({
 \r
 \r
         // Parse any links found\r
-        msg.msg = msg.msg.replace(/(([A-Za-z0-9\-]+\:\/\/)|(www\.))([\w.\-]+)([a-zA-Z]{2,6})(:[0-9]+)?(\/[\w#!:.?$'()[\]*,;~+=&%@!\-\/]*)?/gi, function (url) {\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
@@ -483,10 +557,10 @@ _kiwi.view.Panel = Backbone.View.extend({
     },\r
     chanClick: function (event) {\r
         if (event.target) {\r
-            _kiwi.gateway.join($(event.target).data('channel'));\r
+            _kiwi.gateway.join(null, $(event.target).data('channel'));\r
         } else {\r
             // IE...\r
-            _kiwi.gateway.join($(event.srcElement).data('channel'));\r
+            _kiwi.gateway.join(null, $(event.srcElement).data('channel'));\r
         }\r
     },\r
 \r
@@ -544,7 +618,7 @@ _kiwi.view.Panel = Backbone.View.extend({
         var $this = this.$el;\r
 \r
         // Hide all other panels and show this one\r
-        this.$container.children().css('display', 'none');\r
+        this.$container.children('.panel').css('display', 'none');\r
         $this.css('display', 'block');\r
 \r
         // Show this panels memberlist\r
@@ -557,14 +631,14 @@ _kiwi.view.Panel = Backbone.View.extend({
             $('#memberlists').addClass('disabled').children().removeClass('active');\r
         }\r
 \r
-        _kiwi.app.view.doLayout();\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
-        this.trigger('active', this.model);\r
-        _kiwi.app.panels.trigger('active', this.model, _kiwi.app.panels.active);\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
@@ -572,7 +646,7 @@ _kiwi.view.Panel = Backbone.View.extend({
 \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
+        if (this.model == _kiwi.app.panels().active) return;\r
 \r
         var types, type_idx;\r
         types = ['none', 'action', 'activity', 'highlight'];\r
@@ -609,7 +683,7 @@ _kiwi.view.Panel = Backbone.View.extend({
     // 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
+        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
@@ -619,7 +693,7 @@ _kiwi.view.Panel = Backbone.View.extend({
 });\r
 \r
 _kiwi.view.Applet = _kiwi.view.Panel.extend({\r
-    className: 'applet',\r
+    className: 'panel applet',\r
     initialize: function (options) {\r
         this.initializePanel(options);\r
     }\r
@@ -649,18 +723,50 @@ _kiwi.view.Channel = _kiwi.view.Panel.extend({
         if (typeof topic !== 'string' || !topic) {\r
             topic = this.model.get("topic");\r
         }\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
+        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
@@ -673,31 +779,43 @@ _kiwi.view.Tabs = Backbone.View.extend({
 \r
         this.model.on('active', this.panelActive, this);\r
 \r
-        this.tabs_applets = $('ul.applets', this.$el);\r
-        this.tabs_msg = $('ul.channels', this.$el);\r
+        // Network tabs start with a server, so determine what we are now\r
+        this.is_network = false;\r
 \r
-        _kiwi.gateway.on('change:name', function (gateway, new_val) {\r
-            $('span', this.model.server.tab).text(new_val);\r
-        }, this);\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.tabs_msg.empty();\r
+        this.$el.empty();\r
         \r
-        // Add the server tab first\r
-        this.model.server.tab\r
-            .data('panel_id', this.model.server.cid)\r
-            .appendTo(this.tabs_msg);\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 (panel == that.model.server) return;\r
+            if (this.is_network && panel == that.model.server)\r
+                return;\r
+\r
+            panel.tab.data('panel', panel);\r
 \r
-            panel.tab\r
-                .data('panel_id', panel.cid)\r
-                .appendTo(panel.isApplet() ? this.tabs_applets : this.tabs_msg);\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
@@ -716,10 +834,16 @@ _kiwi.view.Tabs = Backbone.View.extend({
             panel.tab.addClass('icon-nonexistant');\r
         }\r
 \r
-        panel.tab.data('panel_id', panel.cid)\r
-            .appendTo(panel.isApplet() ? this.tabs_applets : this.tabs_msg);\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
@@ -731,9 +855,8 @@ _kiwi.view.Tabs = Backbone.View.extend({
 \r
     panelActive: function (panel, previously_active_panel) {\r
         // Remove any existing tabs or part images\r
-        $('.part', this.$el).remove();\r
-        this.tabs_applets.children().removeClass('active');\r
-        this.tabs_msg.children().removeClass('active');\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
@@ -746,7 +869,7 @@ _kiwi.view.Tabs = Backbone.View.extend({
     tabClick: function (e) {\r
         var tab = $(e.currentTarget);\r
 \r
-        var panel = this.model.getByCid(tab.data('panel_id'));\r
+        var panel = tab.data('panel');\r
         if (!panel) {\r
             // A panel wasn't found for this tab... wadda fuck\r
             return;\r
@@ -757,28 +880,17 @@ _kiwi.view.Tabs = Backbone.View.extend({
 \r
     partClick: function (e) {\r
         var tab = $(e.currentTarget).parent();\r
-        var panel = this.model.getByCid(tab.data('panel_id'));\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
-            _kiwi.gateway.part(panel.get('name'));\r
+            this.model.network.gateway.part(panel.get('name'));\r
         } else {\r
             panel.close();\r
         }\r
-    },\r
-\r
-    next: function () {\r
-        var next = _kiwi.app.panels.active.tab.next();\r
-        if (!next.length) next = $('li:first', this.tabs_msgs);\r
-\r
-        next.click();\r
-    },\r
-    prev: function () {\r
-        var prev = _kiwi.app.panels.active.tab.prev();\r
-        if (!prev.length) prev = $('li:last', this.tabs_msgs);\r
-\r
-        prev.click();\r
     }\r
 });\r
 \r
@@ -809,13 +921,13 @@ _kiwi.view.TopicBar = Backbone.View.extend({
             inp_val = inp.text();\r
         \r
         // Only allow topic editing if this is a channel panel\r
-        if (!_kiwi.app.panels.active.isChannel()) {\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(_kiwi.app.panels.active.get('name'), inp_val);\r
+            _kiwi.gateway.topic(null, _kiwi.app.panels().active.get('name'), inp_val);\r
             return false;\r
         }\r
     },\r
@@ -848,8 +960,18 @@ _kiwi.view.ControlBox = Backbone.View.extend({
         // Hold tab autocomplete data\r
         this.tabcomplete = {active: false, data: [], prefix: ''};\r
 \r
-        _kiwi.gateway.bind('change:nick', function () {\r
-            $('.nick', that.$el).text(this.get('nick'));\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
@@ -899,7 +1021,8 @@ _kiwi.view.ControlBox = Backbone.View.extend({
                 this.buffer_pos--;\r
                 inp.val(this.buffer[this.buffer_pos]);\r
             }\r
-            break;\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
@@ -909,22 +1032,62 @@ _kiwi.view.ControlBox = Backbone.View.extend({
             break;\r
 \r
         case (ev.keyCode === 219 && meta):            // [ + meta\r
-            _kiwi.app.panels.view.prev();\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
-            _kiwi.app.panels.view.next();\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
-                $.each(_kiwi.app.panels.active.get('members').models, function (i, member) {\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
@@ -1003,12 +1166,12 @@ _kiwi.view.ControlBox = Backbone.View.extend({
             command_raw = command_raw.replace(/^\/\//, '/');\r
 \r
             // Prepend the default command\r
-            command_raw = '/msg ' + _kiwi.app.panels.active.get('name') + ' ' + command_raw;\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.gateway.get('name');\r
-        this.preprocessor.vars.channel = _kiwi.app.panels.active.get('name');\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
@@ -1020,7 +1183,7 @@ _kiwi.view.ControlBox = Backbone.View.extend({
         } else {\r
             // Default command\r
             command = 'msg';\r
-            params.unshift(_kiwi.app.panels.active.get('name'));\r
+            params.unshift(_kiwi.app.panels().active.get('name'));\r
         }\r
 \r
         // Trigger the command events\r
@@ -1029,7 +1192,7 @@ _kiwi.view.ControlBox = Backbone.View.extend({
 \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._callbacks['command:' + command]) {\r
+        if (!this._events['command:' + command]) {\r
             this.trigger('unknown_command', {command: command, params: params});\r
         }\r
     },\r
@@ -1555,3 +1718,104 @@ _kiwi.view.MediaMessage = Backbone.View.extend({
         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
index 757b77c06591f8f96be813c2b5c2ae90a2f414ae..1dba81c94a2b26bf8c3ed7617d9dc779471dd3b7 100644 (file)
@@ -1,39 +1,47 @@
-/*!
- Lo-Dash 0.9.1 lodash.com/license
- Underscore.js 1.4.2 underscorejs.org/LICENSE
-*/
-;(function(e,t){function s(e){if(e&&e.__wrapped__)return e;if(!(this instanceof s))return new s(e);this.__wrapped__=e}function o(e,t,n){t||(t=0);var r=e.length,i=r-t>=(n||Q),s=i?{}:e;if(i)for(n=t-1;++n<r;){var o=e[n]+"";(dt.call(s,o)?s[o]:s[o]=[]).push(e[n])}return function(e){if(i){var n=e+"";return dt.call(s,n)&&-1<j(s[n],e)}return-1<j(s,e,t)}}function u(e,n){var r=e.b,i=n.b,e=e.a,n=n.a;if(e!==n){if(e>n||e===t)return 1;if(e<n||n===t)return-1}return r<i?-1:1}function a(e,t,n){function r(){var u=arguments
-,a=s?this:t;return i||(e=t[o]),n.length&&(u=u.length?n.concat(gt.call(u)):n),this instanceof r?(p.prototype=e.prototype,a=new p,(u=e.apply(a,u))&&$t[typeof u]?u:a):e.apply(a,u)}var i=S(e),s=!n,o=e;return s&&(n=t),r}function f(e,n){return e?"function"!=typeof e?function(t){return t[e]}:n!==t?function(t,r,i){return e.call(n,t,r,i)}:e:U}function l(){for(var e={b:"",c:"",e:Ht,f:Xt,g:"",h:Ft,i:Rt,j:ft,k:"",l:n},t,r=0;t=arguments[r];r++)for(var i in t)e[i]=t[i];t=e.a,e.d=/^[^,]+/.exec(t)[0],r="var h,w,j="+
-e.d+",r="+e.d+";if(!"+e.d+")return r;"+e.k+";",e.b?(r+="var k=j.length;h=-1;if(typeof k=='number'){",e.i&&(r+="if(v.call(j)==t){j=j.split('')}"),r+="while(++h<k){w=j[h];"+e.b+"}}else {"):e.h&&(r+="var k=j.length;h=-1;if(k&&i(j)){while(++h<k){w=j[h+=''];"+e.g+"}}else {"),e.e||(r+="var s=typeof j=='function'&&q.call(j,'prototype');");if(e.f&&e.l)r+="var o=-1,p=n[typeof j]?l(j):[],k=p.length;while(++o<k){h=p[o];",e.e||(r+="if(!(s&&h=='prototype')){"),r+="w=j[h];"+e.g+"",e.e||(r+="}");else{r+="for(h in j){"
-;if(!e.e||e.l)r+="if(",e.e||(r+="!(s&&h=='prototype')"),!e.e&&e.l&&(r+="&&"),e.l&&(r+="g.call(j,h)"),r+="){";r+="w=j[h];"+e.g+";";if(!e.e||e.l)r+="}"}r+="}";if(e.e){r+="var f=j.constructor;";for(i=0;7>i;i++)r+="h='"+e.j[i]+"';if(","constructor"==e.j[i]&&(r+="!(f&&f.prototype===j)&&"),r+="g.call(j,h)){w=j[h];"+e.g+"}"}if(e.b||e.h)r+="}";return r+=e.c+";return r",Function("e,g,i,n,l,q,t,v","return function("+t+"){"+r+"}")(f,dt,v,$t,xt,mt,Pt,yt)}function c(e){return"\\"+Jt[e]}function h(e){return en
-[e]}function p(){}function d(e){return tn[e]}function v(e){return yt.call(e)==kt}function m(e){var t=i;if(!e||"object"!=typeof e||v(e))return t;var n=e.constructor;return(!Ut||"function"==typeof e.toString||"string"!=typeof (e+""))&&(!S(n)||n instanceof n)?Bt?(Yt(e,function(e,n,r){return t=!dt.call(r,n),i}),t===i):(Yt(e,function(e,n){t=n}),t===i||dt.call(e,t)):t}function g(e){var t=[];return Zt(e,function(e,n){t.push(n)}),t}function y(e,t,n,s,o){if(e==r)return e;n&&(t=i);if(n=$t[typeof e]){var u=
-yt.call(e);if(!Vt[u]||It&&v(e))return e;var a=u==Lt,n=a||(u==_t?on(e):n)}if(!n||!t)return n?a?gt.call(e):rn({},e):e;n=e.constructor;switch(u){case At:case Ot:return new n(+e);case Mt:case Pt:return new n(e);case Dt:return n(e.source,rt.exec(e))}s||(s=[]),o||(o=[]);for(u=s.length;u--;)if(s[u]==e)return o[u];var f=a?n(e.length):{};return s.push(e),o.push(f),(a?an:Zt)(e,function(e,n){f[n]=y(e,t,r,s,o)}),f}function b(e){var t=[];return Yt(e,function(e,n){S(e)&&t.push(n)}),t.sort()}function w(e){var t=
-{};return Zt(e,function(e,n){t[e]=n}),t}function E(e,t,s,o){if(e===t)return 0!==e||1/e==1/t;if(e==r||t==r)return e===t;var u=yt.call(e);if(u!=yt.call(t))return i;switch(u){case At:case Ot:return+e==+t;case Mt:return e!=+e?t!=+t:0==e?1/e==1/t:e==+t;case Dt:case Pt:return e==t+""}var a=u==Lt||u==kt;if(It&&!a&&(a=v(e))&&!v(t))return i;if(!a){if(e.__wrapped__||t.__wrapped__)return E(e.__wrapped__||e,t.__wrapped__||t);if(u!=_t||Ut&&("function"!=typeof e.toString&&"string"==typeof (e+"")||"function"!=typeof 
-t.toString&&"string"==typeof (t+"")))return i;var u=e.constructor,f=t.constructor;if(u!=f&&(!S(u)||!(u instanceof u&&S(f)&&f instanceof f)))return i}s||(s=[]),o||(o=[]);for(u=s.length;u--;)if(s[u]==e)return o[u]==t;var u=-1,f=n,l=0;s.push(e),o.push(t);if(a){l=e.length;if(f=l==t.length)for(;l--&&(f=E(e[l],t[l],s,o)););return f}for(var c in e)if(dt.call(e,c)&&(l++,!dt.call(t,c)||!E(e[c],t[c],s,o)))return i;for(c in t)if(dt.call(t,c)&&!(l--))return i;if(Ht)for(;7>++u;)if(c=ft[u],dt.call(e,c)&&(!dt.call
-(t,c)||!E(e[c],t[c],s,o)))return i;return n}function S(e){return"function"==typeof e}function x(e,t,n){var i=arguments,s=0,o=2,u=i[3],a=i[4];n!==K&&(u=[],a=[],o=i.length);for(;++s<o;)Zt(i[s],function(t,n){var i,s,o;if(t&&((s=sn(t))||on(t))){for(var f=u.length;f--;)if(i=u[f]==t)break;i?e[n]=a[f]:(u.push(t),a.push(o=(o=e[n],s)?sn(o)?o:[]:on(o)?o:{}),e[n]=x(o,t,K,u,a))}else t!=r&&(e[n]=t)});return e}function T(e){var t=[];return Zt(e,function(e){t.push(e)}),t}function N(e,t){return"number"==typeof (
-e?e.length:0)?-1<(yt.call(e)==Pt?e.indexOf(t):j(e,t)):P(e,function(e){return e===t})}function C(e,t,r){var i=n,t=f(t,r);return an(e,function(e,n,r){return i=!!t(e,n,r)}),i}function k(e,t,n){var r=[],t=f(t,n);return an(e,function(e,n,i){t(e,n,i)&&r.push(e)}),r}function L(e,t,r){var i,t=f(t,r);return P(e,function(e,r,s){return t(e,r,s)&&(i=e,n)}),i}function A(e,t,n){var r=-1,i=e?e.length:0,s=Array("number"==typeof i?i:0),t=f(t,n);if(sn(e))for(;++r<i;)s[r]=t(e[r],r,e);else an(e,function(e,n,i){s[++r
-]=t(e,n,i)});return s}function O(e,t,n){var r=-Infinity,i=-1,s=e?e.length:0,o=r;if(t||"number"!=typeof s)t=f(t,n),an(e,function(e,n,i){n=t(e,n,i),n>r&&(r=n,o=e)});else for(;++i<s;)e[i]>o&&(o=e[i]);return o}function M(e,t){var n=[];return an(e,function(e){n.push(e[t])}),n}function _(e,t,n,r){var s=3>arguments.length,t=f(t,r);return an(e,function(e,r,o){n=s?(s=i,e):t(n,e,r,o)}),n}function D(e,t,n,r){var s=e,o=e?e.length:0,u=3>arguments.length;if("number"!=typeof o)var a=un(e),o=a.length;else Rt&&yt
-.call(e)==Pt&&(s=e.split(""));return an(e,function(e,f,l){f=a?a[--o]:--o,n=u?(u=i,s[f]):t.call(r,n,s[f],f,l)}),n}function P(e,t,n){var r,t=f(t,n);return an(e,function(e,n,i){return!(r=t(e,n,i))}),!!r}function H(e,t,n){if(e)return t==r||n?e[0]:gt.call(e,0,t)}function B(e,t){for(var n=-1,r=e?e.length:0,i=[];++n<r;){var s=e[n];sn(s)?vt.apply(i,t?s:B(s)):i.push(s)}return i}function j(e,t,n){var r=-1,i=e?e.length:0;if("number"==typeof n)r=(0>n?Tt(0,i+n):n||0)-1;else if(n)return r=I(e,t),e[r]===t?r:-1;
-for(;++r<i;)if(e[r]===t)return r;return-1}function F(e,t,n){return e?gt.call(e,t==r||n?1:t):[]}function I(e,t,n,r){for(var i=0,s=e?e.length:i,n=n?f(n,r):U,t=n(t);i<s;)r=i+s>>>1,n(e[r])<t?i=r+1:s=r;return i}function q(e,t,n,r){var s=-1,o=e?e.length:0,u=[],a=u;"function"==typeof t&&(r=n,n=t,t=i),n&&(a=[],n=f(n,r));for(;++s<o;){var r=e[s],l=n?n(r,s,e):r;if(t?!s||a[a.length-1]!==l:0>j(a,l))n&&a.push(l),u.push(r)}return u}function R(e,t){return Wt||bt&&2<arguments.length?bt.call.apply(bt,arguments):a(
-e,t,gt.call(arguments,2))}function U(e){return e}function z(e){an(b(e),function(t){var r=s[t]=e[t];s.prototype[t]=function(){var e=[this.__wrapped__];return vt.apply(e,arguments),e=r.apply(s,e),this.__chain__&&(e=new s(e),e.__chain__=n),e}})}var n=!0,r=null,i=!1,W="object"==typeof exports&&exports,X="object"==typeof global&&global;X.global===X&&(e=X);var V=[],$={},J=0,K={},Q=30,G=e._,Y=/[-?+=!~*%&^<>|{(\/]|\[\D|\b(?:delete|in|instanceof|new|typeof|void)\b/,Z=/&(?:amp|lt|gt|quot|#x27);/g,et=/\b__p\+='';/g
-,tt=/\b(__p\+=)''\+/g,nt=/(__e\(.*?\)|\b__t\))\+'';/g,rt=/\w*$/,it=/(?:__e|__t=)\(\s*(?![\d\s"']|this\.)/g,st=RegExp("^"+($.valueOf+"").replace(/[.*+?^=!:${}()|[\]\/\\]/g,"\\$&").replace(/valueOf|for [^\]]+/g,".+?")+"$"),ot=/($^)/,ut=/[&<>"']/g,at=/['\n\r\t\u2028\u2029\\]/g,ft="constructor hasOwnProperty isPrototypeOf propertyIsEnumerable toLocaleString toString valueOf".split(" "),lt=Math.ceil,ct=V.concat,ht=Math.floor,pt=st.test(pt=Object.getPrototypeOf)&&pt,dt=$.hasOwnProperty,vt=V.push,mt=$.propertyIsEnumerable
-,gt=V.slice,yt=$.toString,bt=st.test(bt=gt.bind)&&bt,wt=st.test(wt=Array.isArray)&&wt,Et=e.isFinite,St=e.isNaN,xt=st.test(xt=Object.keys)&&xt,Tt=Math.max,Nt=Math.min,Ct=Math.random,kt="[object Arguments]",Lt="[object Array]",At="[object Boolean]",Ot="[object Date]",Mt="[object Number]",_t="[object Object]",Dt="[object RegExp]",Pt="[object String]",Ht,Bt,jt=(jt={0:1,length:1},V.splice.call(jt,0,1),jt[0]),Ft=n;(function(){function e(){this.x=1}var t=[];e.prototype={valueOf:1,y:1};for(var n in new e
-)t.push(n);for(n in arguments)Ft=!n;Ht=!/valueOf/.test(t),Bt="x"!=t[0]})(1);var It=!v(arguments),qt="x"!=gt.call("x")[0],Rt="xx"!="x"[0]+Object("x")[0];try{var Ut=("[object Object]",yt.call(e.document||0)==_t)}catch(zt){}var Wt=bt&&/\n|Opera/.test(bt+yt.call(e.opera)),Xt=xt&&/^.+$|true/.test(xt+!!e.attachEvent),Vt={};Vt[kt]=Vt["[object Function]"]=i,Vt[Lt]=Vt[At]=Vt[Ot]=Vt[Mt]=Vt[_t]=Vt[Dt]=Vt[Pt]=n;var $t={"boolean":i,"function":n,object:n,number:i,string:i,"undefined":i},Jt={"\\":"\\","'":"'","\n"
-:"n","\r":"r","        ":"t","\u2028":"u2028","\u2029":"u2029"};s.templateSettings={escape:/<%-([\s\S]+?)%>/g,evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,variable:""};var Kt={a:"d,c,u",k:"c=e(c,u)",b:"if(c(w,h,d)===false)return r",g:"if(c(w,h,d)===false)return r"},Qt={l:i,a:"m",k:"for(var a=1,b=arguments.length;a<b;a++){if(j=arguments[a]){",g:"r[h]=w",c:"}}"},Gt={b:r};It&&(v=function(e){return e?dt.call(e,"callee"):i});var Yt=l(Kt,Gt,{l:i}),Zt=l(Kt,Gt),en={"&":"&amp;","<":"&lt;",">":"&gt;"
-,'"':"&quot;","'":"&#x27;"},tn=w(en),nn=l(Qt,{g:"if(r[h]==null)"+Qt.g}),rn=l(Qt),sn=wt||function(e){return yt.call(e)==Lt};S(/x/)&&(S=function(e){return"[object Function]"==yt.call(e)});var on=pt?function(e){if(!e||"object"!=typeof e)return i;var t=e.valueOf,n="function"==typeof t&&(n=pt(t))&&pt(n);return n?e==n||pt(e)==n&&!v(e):m(e)}:m,un=xt?function(e){var t=typeof e;return"function"==t&&mt.call(e,"prototype")?g(e):e&&$t[t]?xt(e):[]}:g,an=l(Kt);s.VERSION="0.9.1",s.after=function(e,t){return 1>e?
-t():function(){if(1>--e)return t.apply(this,arguments)}},s.bind=R,s.bindAll=function(e){for(var t=arguments,n=1<t.length?0:(t=b(e),-1),r=t.length;++n<r;){var i=t[n];e[i]=R(e[i],e)}return e},s.chain=function(e){return e=new s(e),e.__chain__=n,e},s.clone=y,s.compact=function(e){for(var t=-1,n=e?e.length:0,r=[];++t<n;){var i=e[t];i&&r.push(i)}return r},s.compose=function(){var e=arguments;return function(){for(var t=arguments,n=e.length;n--;)t=[e[n].apply(this,t)];return t[0]}},s.contains=N,s.countBy=
-function(e,t,n){var r={},t=f(t,n);return an(e,function(e,n,i){n=t(e,n,i),dt.call(r,n)?r[n]++:r[n]=1}),r},s.debounce=function(e,t,n){function i(){a=r,n||(o=e.apply(u,s))}var s,o,u,a;return function(){var r=n&&!a;return s=arguments,u=this,clearTimeout(a),a=setTimeout(i,t),r&&(o=e.apply(u,s)),o}},s.defaults=nn,s.defer=function(e){var n=gt.call(arguments,1);return setTimeout(function(){e.apply(t,n)},1)},s.delay=function(e,n){var r=gt.call(arguments,2);return setTimeout(function(){e.apply(t,r)},n)},s.
-difference=function(e){for(var t=-1,n=e?e.length:0,r=ct.apply(V,arguments),r=o(r,n),i=[];++t<n;){var s=e[t];r(s)||i.push(s)}return i},s.escape=function(e){return e==r?"":(e+"").replace(ut,h)},s.every=C,s.extend=rn,s.filter=k,s.find=L,s.first=H,s.flatten=B,s.forEach=an,s.forIn=Yt,s.forOwn=Zt,s.functions=b,s.groupBy=function(e,t,n){var r={},t=f(t,n);return an(e,function(e,n,i){n=t(e,n,i),(dt.call(r,n)?r[n]:r[n]=[]).push(e)}),r},s.has=function(e,t){return e?dt.call(e,t):i},s.identity=U,s.indexOf=j,s
-.initial=function(e,t,n){return e?gt.call(e,0,-(t==r||n?1:t)):[]},s.intersection=function(e){var t=arguments,n=t.length,r={},i=[];return an(e,function(e){if(0>j(i,e)){for(var s=n;--s;)if(!(r[s]||(r[s]=o(t[s])))(e))return;i.push(e)}}),i},s.invert=w,s.invoke=function(e,t){var n=gt.call(arguments,2),r="function"==typeof t,i=[];return an(e,function(e){i.push((r?t:e[t]).apply(e,n))}),i},s.isArguments=v,s.isArray=sn,s.isBoolean=function(e){return e===n||e===i||yt.call(e)==At},s.isDate=function(e){return yt
-.call(e)==Ot},s.isElement=function(e){return e?1===e.nodeType:i},s.isEmpty=function(e){var t=n;if(!e)return t;var r=yt.call(e),s=e.length;return r==Lt||r==Pt||r==kt||It&&v(e)||r==_t&&"number"==typeof s&&S(e.splice)?!s:(Zt(e,function(){return t=i}),t)},s.isEqual=E,s.isFinite=function(e){return Et(e)&&!St(parseFloat(e))},s.isFunction=S,s.isNaN=function(e){return yt.call(e)==Mt&&e!=+e},s.isNull=function(e){return e===r},s.isNumber=function(e){return yt.call(e)==Mt},s.isObject=function(e){return e?$t
-[typeof e]:i},s.isPlainObject=on,s.isRegExp=function(e){return yt.call(e)==Dt},s.isString=function(e){return yt.call(e)==Pt},s.isUndefined=function(e){return e===t},s.keys=un,s.last=function(e,t,n){if(e){var i=e.length;return t==r||n?e[i-1]:gt.call(e,-t||i)}},s.lastIndexOf=function(e,t,n){var r=e?e.length:0;for("number"==typeof n&&(r=(0>n?Tt(0,r+n):Nt(n,r-1))+1);r--;)if(e[r]===t)return r;return-1},s.lateBind=function(e,t){return a(t,e,gt.call(arguments,2))},s.map=A,s.max=O,s.memoize=function(e,t)
-{var n={};return function(){var r=t?t.apply(this,arguments):arguments[0];return dt.call(n,r)?n[r]:n[r]=e.apply(this,arguments)}},s.merge=x,s.min=function(e,t,n){var r=Infinity,i=-1,s=e?e.length:0,o=r;if(t||"number"!=typeof s)t=f(t,n),an(e,function(e,n,i){n=t(e,n,i),n<r&&(r=n,o=e)});else for(;++i<s;)e[i]<o&&(o=e[i]);return o},s.mixin=z,s.noConflict=function(){return e._=G,this},s.object=function(e,t){for(var n=-1,r=e?e.length:0,i={};++n<r;){var s=e[n];t?i[s]=t[n]:i[s[0]]=s[1]}return i},s.omit=function(
-e,t,n){var r="function"==typeof t,i={};if(r)t=f(t,n);else var s=ct.apply(V,arguments);return Yt(e,function(e,n,o){if(r?!t(e,n,o):0>j(s,n,1))i[n]=e}),i},s.once=function(e){var t,s=i;return function(){return s?t:(s=n,t=e.apply(this,arguments),e=r,t)}},s.pairs=function(e){var t=[];return Zt(e,function(e,n){t.push([n,e])}),t},s.partial=function(e){return a(e,gt.call(arguments,1))},s.pick=function(e,t,n){var r={};if("function"!=typeof t)for(var i=0,s=ct.apply(V,arguments),o=s.length;++i<o;){var u=s[i]
-;u in e&&(r[u]=e[u])}else t=f(t,n),Yt(e,function(e,n,i){t(e,n,i)&&(r[n]=e)});return r},s.pluck=M,s.random=function(e,t){return e==r&&t==r&&(t=1),e=+e||0,t==r&&(t=e,e=0),e+ht(Ct()*((+t||0)-e+1))},s.range=function(e,t,n){e=+e||0,n=+n||1,t==r&&(t=e,e=0);for(var i=-1,t=Tt(0,lt((t-e)/n)),s=Array(t);++i<t;)s[i]=e,e+=n;return s},s.reduce=_,s.reduceRight=D,s.reject=function(e,t,n){return t=f(t,n),k(e,function(e,n,r){return!t(e,n,r)})},s.rest=F,s.result=function(e,t){var n=e?e[t]:r;return S(n)?e[t]():n},s
-.shuffle=function(e){var t=-1,n=Array(e?e.length:0);return an(e,function(e){var r=ht(Ct()*(++t+1));n[t]=n[r],n[r]=e}),n},s.size=function(e){var t=e?e.length:0;return"number"==typeof t?t:un(e).length},s.some=P,s.sortBy=function(e,t,n){var r=[],t=f(t,n);an(e,function(e,n,i){r.push({a:t(e,n,i),b:n,c:e})}),e=r.length;for(r.sort(u);e--;)r[e]=r[e].c;return r},s.sortedIndex=I,s.tap=function(e,t){return t(e),e},s.template=function(e,t,n){e||(e=""),n||(n={});var r,i,o=0,u=s.templateSettings,a="__p += '",f=
-n.variable||u.variable,l=f;e.replace(RegExp((n.escape||u.escape||ot).source+"|"+(n.interpolate||u.interpolate||ot).source+"|"+(n.evaluate||u.evaluate||ot).source+"|$","g"),function(t,n,i,s,u){a+=e.slice(o,u).replace(at,c),a+=n?"'+__e("+n+")+'":s?"';"+s+";__p+='":i?"'+((__t=("+i+"))==null?'':__t)+'":"",r||(r=s||Y.test(n||i)),o=u+t.length}),a+="';",l||(f="obj",r?a="with("+f+"){"+a+"}":(n=RegExp("(\\(\\s*)"+f+"\\."+f+"\\b","g"),a=a.replace(it,"$&"+f+".").replace(n,"$1__d"))),a=(r?a.replace(et,""):a)
-.replace(tt,"$1").replace(nt,"$1;"),a="function("+f+"){"+(l?"":f+"||("+f+"={});")+"var __t,__p='',__e=_.escape"+(r?",__j=[].join;function print(){__p+=__j.call(arguments,'')}":(l?"":",__d="+f+"."+f+"||"+f)+";")+a+"return __p}";try{i=Function("_","return "+a)(s)}catch(h){throw h.source=a,h}return t?i(t):(i.source=a,i)},s.throttle=function(e,t){function n(){a=new Date,u=r,s=e.apply(o,i)}var i,s,o,u,a=0;return function(){var r=new Date,f=t-(r-a);return i=arguments,o=this,0>=f?(clearTimeout
-(u),a=r,s=e.apply(o,i)):u||(u=setTimeout(n,f)),s}},s.times=function(e,t,n){for(var e=+e||0,r=-1,i=Array(e);++r<e;)i[r]=t.call(n,r);return i},s.toArray=function(e){return e&&"number"==typeof e.length?(qt?yt.call(e)==Pt:"string"==typeof e)?e.split(""):gt.call(e):T(e)},s.unescape=function(e){return e==r?"":(e+"").replace(Z,d)},s.union=function(){return q(ct.apply(V,arguments))},s.uniq=q,s.uniqueId=function(e){var t=J++;return e?e+t:t},s.values=T,s.where=function(e,t){var n=[];return Yt(t,function(e,
-t){n.push(t)}),k(e,function(e){for(var r=n.length;r--;){var i=e[n[r]]===t[n[r]];if(!i)break}return!!i})},s.without=function(e){for(var t=-1,n=e?e.length:0,r=o(arguments,1,20),i=[];++t<n;){var s=e[t];r(s)||i.push(s)}return i},s.wrap=function(e,t){return function(){var n=[e];return vt.apply(n,arguments),t.apply(this,n)}},s.zip=function(e){for(var t=-1,n=e?O(M(arguments,"length")):0,r=Array(n);++t<n;)r[t]=M(arguments,t);return r},s.all=C,s.any=P,s.collect=A,s.detect=L,s.drop=F,s.each=an,s.foldl=_,s.
-foldr=D,s.head=H,s.include=N,s.inject=_,s.methods=b,s.select=k,s.tail=F,s.take=H,s.unique=q,z(s),s.prototype.chain=function(){return this.__chain__=n,this},s.prototype.value=function(){return this.__wrapped__},an("pop push reverse shift sort splice unshift".split(" "),function(e){var t=V[e];s.prototype[e]=function(){var e=this.__wrapped__;return t.apply(e,arguments),jt&&e.length===0&&delete e[0],this.__chain__&&(e=new s(e),e.__chain__=n),e}}),an(["concat","join","slice"],function(e){var t=V[e];s.
-prototype[e]=function(){var e=t.apply(this.__wrapped__,arguments);return this.__chain__&&(e=new s(e),e.__chain__=n),e}}),typeof define=="function"&&typeof define.amd=="object"&&define.amd?(e._=s,define(function(){return s})):W?"object"==typeof module&&module&&module.exports==W?(module.exports=s)._=s:W._=s:e._=s})(this);
\ No newline at end of file
+/**
+ * @license
+ * Lo-Dash 1.2.1 (Custom Build) lodash.com/license
+ * Build: `lodash -o ./dist/lodash.compat.js`
+ * Underscore.js 1.4.4 underscorejs.org/LICENSE
+ */
+;(function(n){function t(r){function a(n){return n&&typeof n=="object"&&!me(n)&&Qt.call(n,"__wrapped__")?n:new V(n)}function R(n){var t=n.length,e=t>=l;if(e)for(var r={},u=-1;++u<t;){var a=f+n[u];(r[a]||(r[a]=[])).push(n[u])}return function(t){if(e){var u=f+t;return r[u]&&-1<_t(r[u],t)}return-1<_t(n,t)}}function T(n){return n.charCodeAt(0)}function D(n,t){var e=n.b,r=t.b;if(n=n.a,t=t.a,n!==t){if(n>t||typeof n=="undefined")return 1;if(n<t||typeof t=="undefined")return-1}return e<r?-1:1}function z(n,t,e,r){function u(){var r=arguments,l=o?this:t;
+return a||(n=t[i]),e.length&&(r=r.length?(r=le.call(r),f?r.concat(e):e.concat(r)):e),this instanceof u?(G.prototype=n.prototype,l=new G,G.prototype=null,r=n.apply(l,r),et(r)?r:l):n.apply(l,r)}var a=tt(n),o=!e,i=t;if(o){var f=r;e=t}else if(!a){if(!r)throw new Dt;t=n}return u}function L(){for(var n,t={g:j,b:"k(m)",c:"",e:"m",f:"",h:"",i:!0,j:!!be},e=0;n=arguments[e];e++)for(var r in n)t[r]=n[r];if(n=t.a,t.d=/^[^,]+/.exec(n)[0],e=$t,r="var i,m="+t.d+",u="+t.e+";if(!m)return u;"+t.h+";",t.b?(r+="var n=m.length;i=-1;if("+t.b+"){",ve.unindexedChars&&(r+="if(l(m)){m=m.split('')}"),r+="while(++i<n){"+t.f+"}}else{"):ve.nonEnumArgs&&(r+="var n=m.length;i=-1;if(n&&j(m)){while(++i<n){i+='';"+t.f+"}}else{"),ve.enumPrototypes&&(r+="var v=typeof m=='function';"),t.i&&t.j)r+="var s=-1,t=r[typeof m]?o(m):[],n=t.length;while(++s<n){i=t[s];",ve.enumPrototypes&&(r+="if(!(v&&i=='prototype')){"),r+=t.f,ve.enumPrototypes&&(r+="}"),r+="}";
+else if(r+="for(i in m){",(ve.enumPrototypes||t.i)&&(r+="if(",ve.enumPrototypes&&(r+="!(v&&i=='prototype')"),ve.enumPrototypes&&t.i&&(r+="&&"),t.i&&(r+="h.call(m,i)"),r+="){"),r+=t.f+";",(ve.enumPrototypes||t.i)&&(r+="}"),r+="}",ve.nonEnumShadows){r+="var f=m.constructor;";for(var u=0;7>u;u++)r+="i='"+t.g[u]+"';if(","constructor"==t.g[u]&&(r+="!(f&&f.prototype===m)&&"),r+="h.call(m,i)){"+t.f+"}"}return(t.b||ve.nonEnumArgs)&&(r+="}"),r+=t.c+";return u",e("h,j,k,l,o,p,r","return function("+n+"){"+r+"}")(Qt,W,me,ut,be,a,q)
+}function K(n){return"\\"+B[n]}function M(n){return we[n]}function U(n){return typeof n.toString!="function"&&typeof(n+"")=="string"}function V(n){this.__wrapped__=n}function G(){}function H(n){var t=!1;if(!n||Zt.call(n)!=I||!ve.argsClass&&W(n))return t;var e=n.constructor;return(tt(e)?e instanceof e:ve.nodeClass||!U(n))?ve.ownLast?(xe(n,function(n,e,r){return t=Qt.call(r,e),!1}),!0===t):(xe(n,function(n,e){t=e}),!1===t||Qt.call(n,t)):t}function J(n,t,e){t||(t=0),typeof e=="undefined"&&(e=n?n.length:0);
+var r=-1;e=e-t||0;for(var u=It(0>e?0:e);++r<e;)u[r]=n[t+r];return u}function Q(n){return Ce[n]}function W(n){return Zt.call(n)==k}function X(n,t,r,u,o,i){var f=n;if(typeof t=="function"&&(u=r,r=t,t=!1),typeof r=="function"){if(r=typeof u=="undefined"?r:a.createCallback(r,u,1),f=r(f),typeof f!="undefined")return f;f=n}if(u=et(f)){var l=Zt.call(f);if(!$[l]||!ve.nodeClass&&U(f))return f;var c=me(f)}if(!u||!t)return u?c?J(f):je({},f):f;switch(u=se[l],l){case O:case E:return new u(+f);case A:case N:return new u(f);
+case P:return u(f.source,y.exec(f))}for(o||(o=[]),i||(i=[]),l=o.length;l--;)if(o[l]==n)return i[l];return f=c?u(f.length):{},c&&(Qt.call(n,"index")&&(f.index=n.index),Qt.call(n,"input")&&(f.input=n.input)),o.push(n),i.push(f),(c?pt:Oe)(n,function(n,u){f[u]=X(n,t,r,e,o,i)}),f}function Y(n){var t=[];return xe(n,function(n,e){tt(n)&&t.push(e)}),t.sort()}function Z(n){for(var t=-1,e=be(n),r=e.length,u={};++t<r;){var a=e[t];u[n[a]]=a}return u}function nt(n,t,e,r,u,o){var f=e===i;if(typeof e=="function"&&!f){e=a.createCallback(e,r,2);
+var l=e(n,t);if(typeof l!="undefined")return!!l}if(n===t)return 0!==n||1/n==1/t;var c=typeof n,p=typeof t;if(n===n&&(!n||"function"!=c&&"object"!=c)&&(!t||"function"!=p&&"object"!=p))return!1;if(null==n||null==t)return n===t;if(p=Zt.call(n),c=Zt.call(t),p==k&&(p=I),c==k&&(c=I),p!=c)return!1;switch(p){case O:case E:return+n==+t;case A:return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case P:case N:return n==Tt(t)}if(c=p==x,!c){if(Qt.call(n,"__wrapped__")||Qt.call(t,"__wrapped__"))return nt(n.__wrapped__||n,t.__wrapped__||t,e,r,u,o);
+if(p!=I||!ve.nodeClass&&(U(n)||U(t)))return!1;var p=!ve.argsObject&&W(n)?Ft:n.constructor,s=!ve.argsObject&&W(t)?Ft:t.constructor;if(p!=s&&(!tt(p)||!(p instanceof p&&tt(s)&&s instanceof s)))return!1}for(u||(u=[]),o||(o=[]),p=u.length;p--;)if(u[p]==n)return o[p]==t;var v=0,l=!0;if(u.push(n),o.push(t),c){if(p=n.length,v=t.length,l=v==n.length,!l&&!f)return l;for(;v--;)if(c=p,s=t[v],f)for(;c--&&!(l=nt(n[c],s,e,r,u,o)););else if(!(l=nt(n[v],s,e,r,u,o)))break;return l}return xe(t,function(t,a,i){return Qt.call(i,a)?(v++,l=Qt.call(n,a)&&nt(n[a],t,e,r,u,o)):void 0
+}),l&&!f&&xe(n,function(n,t,e){return Qt.call(e,t)?l=-1<--v:void 0}),l}function tt(n){return typeof n=="function"}function et(n){return n?q[typeof n]:!1}function rt(n){return typeof n=="number"||Zt.call(n)==A}function ut(n){return typeof n=="string"||Zt.call(n)==N}function at(n,t,e){var r=arguments,u=0,o=2;if(!et(n))return n;if(e===i)var f=r[3],l=r[4],c=r[5];else l=[],c=[],typeof e!="number"&&(o=r.length),3<o&&"function"==typeof r[o-2]?f=a.createCallback(r[--o-1],r[o--],2):2<o&&"function"==typeof r[o-1]&&(f=r[--o]);
+for(;++u<o;)(me(r[u])?pt:Oe)(r[u],function(t,e){var r,u,a=t,o=n[e];if(t&&((u=me(t))||Ee(t))){for(a=l.length;a--;)if(r=l[a]==t){o=c[a];break}if(!r){var p;f&&(a=f(o,t),p=typeof a!="undefined")&&(o=a),p||(o=u?me(o)?o:[]:Ee(o)?o:{}),l.push(t),c.push(o),p||(o=at(o,t,i,f,l,c))}}else f&&(a=f(o,t),typeof a=="undefined"&&(a=t)),typeof a!="undefined"&&(o=a);n[e]=o});return n}function ot(n){for(var t=-1,e=be(n),r=e.length,u=It(r);++t<r;)u[t]=n[e[t]];return u}function it(n,t,e){var r=-1,u=n?n.length:0,a=!1;return e=(0>e?ae(0,u+e):e)||0,typeof u=="number"?a=-1<(ut(n)?n.indexOf(t,e):_t(n,t,e)):_e(n,function(n){return++r<e?void 0:!(a=n===t)
+}),a}function ft(n,t,e){var r=!0;if(t=a.createCallback(t,e),me(n)){e=-1;for(var u=n.length;++e<u&&(r=!!t(n[e],e,n)););}else _e(n,function(n,e,u){return r=!!t(n,e,u)});return r}function lt(n,t,e){var r=[];if(t=a.createCallback(t,e),me(n)){e=-1;for(var u=n.length;++e<u;){var o=n[e];t(o,e,n)&&r.push(o)}}else _e(n,function(n,e,u){t(n,e,u)&&r.push(n)});return r}function ct(n,t,e){if(t=a.createCallback(t,e),!me(n)){var r;return _e(n,function(n,e,u){return t(n,e,u)?(r=n,!1):void 0}),r}e=-1;for(var u=n.length;++e<u;){var o=n[e];
+if(t(o,e,n))return o}}function pt(n,t,e){if(t&&typeof e=="undefined"&&me(n)){e=-1;for(var r=n.length;++e<r&&!1!==t(n[e],e,n););}else _e(n,t,e);return n}function st(n,t,e){var r=-1,u=n?n.length:0,o=It(typeof u=="number"?u:0);if(t=a.createCallback(t,e),me(n))for(;++r<u;)o[r]=t(n[r],r,n);else _e(n,function(n,e,u){o[++r]=t(n,e,u)});return o}function vt(n,t,e){var r=-1/0,u=r;if(!t&&me(n)){e=-1;for(var o=n.length;++e<o;){var i=n[e];i>u&&(u=i)}}else t=!t&&ut(n)?T:a.createCallback(t,e),_e(n,function(n,e,a){e=t(n,e,a),e>r&&(r=e,u=n)
+});return u}function gt(n,t,e,r){var u=3>arguments.length;if(t=a.createCallback(t,r,4),me(n)){var o=-1,i=n.length;for(u&&(e=n[++o]);++o<i;)e=t(e,n[o],o,n)}else _e(n,function(n,r,a){e=u?(u=!1,n):t(e,n,r,a)});return e}function yt(n,t,e,r){var u=n,o=n?n.length:0,i=3>arguments.length;if(typeof o!="number")var f=be(n),o=f.length;else ve.unindexedChars&&ut(n)&&(u=n.split(""));return t=a.createCallback(t,r,4),pt(n,function(n,r,a){r=f?f[--o]:--o,e=i?(i=!1,u[r]):t(e,u[r],r,a)}),e}function ht(n,t,e){var r;
+if(t=a.createCallback(t,e),me(n)){e=-1;for(var u=n.length;++e<u&&!(r=t(n[e],e,n)););}else _e(n,function(n,e,u){return!(r=t(n,e,u))});return!!r}function mt(n){for(var t=-1,e=n?n.length:0,r=Gt.apply(zt,le.call(arguments,1)),r=R(r),u=[];++t<e;){var a=n[t];r(a)||u.push(a)}return u}function dt(n,t,e){if(n){var r=0,u=n.length;if(typeof t!="number"&&null!=t){var o=-1;for(t=a.createCallback(t,e);++o<u&&t(n[o],o,n);)r++}else if(r=t,null==r||e)return n[0];return J(n,0,oe(ae(0,r),u))}}function bt(n,t,e,r){var u=-1,o=n?n.length:0,i=[];
+for(typeof t!="boolean"&&null!=t&&(r=e,e=t,t=!1),null!=e&&(e=a.createCallback(e,r));++u<o;)r=n[u],e&&(r=e(r,u,n)),me(r)?Wt.apply(i,t?r:bt(r)):i.push(r);return i}function _t(n,t,e){var r=-1,u=n?n.length:0;if(typeof e=="number")r=(0>e?ae(0,u+e):e||0)-1;else if(e)return r=Ct(n,t),n[r]===t?r:-1;for(;++r<u;)if(n[r]===t)return r;return-1}function wt(n,t,e){if(typeof t!="number"&&null!=t){var r=0,u=-1,o=n?n.length:0;for(t=a.createCallback(t,e);++u<o&&t(n[u],u,n);)r++}else r=null==t||e?1:ae(0,t);return J(n,r)
+}function Ct(n,t,e,r){var u=0,o=n?n.length:u;for(e=e?a.createCallback(e,r,1):Et,t=e(t);u<o;)r=u+o>>>1,e(n[r])<t?u=r+1:o=r;return u}function jt(n,t,e,r){var u=-1,o=n?n.length:0,i=[],c=i;typeof t!="boolean"&&null!=t&&(r=e,e=t,t=!1);var p=!t&&o>=l;if(p)var s={};for(null!=e&&(c=[],e=a.createCallback(e,r));++u<o;){r=n[u];var v=e?e(r,u,n):r;if(p)var g=f+v,g=s[g]?!(c=s[g]):c=s[g]=[];(t?!u||c[c.length-1]!==v:g||0>_t(c,v))&&((e||p)&&c.push(v),i.push(r))}return i}function kt(n,t){for(var e=-1,r=n?n.length:0,u={};++e<r;){var a=n[e];
+t?u[a]=t[e]:u[a[0]]=a[1]}return u}function xt(n,t){return ve.fastBind||ne&&2<arguments.length?ne.call.apply(ne,arguments):z(n,t,le.call(arguments,2))}function Ot(n){var t=le.call(arguments,1);return Yt(function(){n.apply(e,t)},1)}function Et(n){return n}function St(n){pt(Y(n),function(t){var e=a[t]=n[t];a.prototype[t]=function(){var n=this.__wrapped__,t=[n];return Wt.apply(t,arguments),t=e.apply(a,t),n&&typeof n=="object"&&n==t?this:new V(t)}})}function At(){return this.__wrapped__}r=r?F.defaults(n.Object(),r,F.pick(n,C)):n;
+var It=r.Array,Pt=r.Boolean,Nt=r.Date,$t=r.Function,qt=r.Math,Bt=r.Number,Ft=r.Object,Rt=r.RegExp,Tt=r.String,Dt=r.TypeError,zt=It(),Lt=Ft(),Kt=r._,Mt=Rt("^"+Tt(Lt.valueOf).replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/valueOf|for [^\]]+/g,".+?")+"$"),Ut=qt.ceil,Vt=r.clearTimeout,Gt=zt.concat,Ht=qt.floor,Jt=Mt.test(Jt=Ft.getPrototypeOf)&&Jt,Qt=Lt.hasOwnProperty,Wt=zt.push,Xt=r.setImmediate,Yt=r.setTimeout,Zt=Lt.toString,ne=Mt.test(ne=Zt.bind)&&ne,te=Mt.test(te=It.isArray)&&te,ee=r.isFinite,re=r.isNaN,ue=Mt.test(ue=Ft.keys)&&ue,ae=qt.max,oe=qt.min,ie=r.parseInt,fe=qt.random,le=zt.slice,ce=Mt.test(r.attachEvent),pe=ne&&!/\n|true/.test(ne+ce),se={};
+se[x]=It,se[O]=Pt,se[E]=Nt,se[I]=Ft,se[A]=Bt,se[P]=Rt,se[N]=Tt;var ve=a.support={};(function(){var n=function(){this.x=1},t={0:1,length:1},e=[];n.prototype={valueOf:1,y:1};for(var r in new n)e.push(r);for(r in arguments);ve.argsObject=arguments.constructor==Ft&&!(arguments instanceof It),ve.argsClass=W(arguments),ve.enumPrototypes=n.propertyIsEnumerable("prototype"),ve.fastBind=ne&&!pe,ve.ownLast="x"!=e[0],ve.nonEnumArgs=0!=r,ve.nonEnumShadows=!/valueOf/.test(e),ve.spliceObjects=(zt.splice.call(t,0,1),!t[0]),ve.unindexedChars="xx"!="x"[0]+Ft("x")[0];
+try{ve.nodeClass=!(Zt.call(document)==I&&!({toString:0}+""))}catch(u){ve.nodeClass=!0}})(1),a.templateSettings={escape:/<%-([\s\S]+?)%>/g,evaluate:/<%([\s\S]+?)%>/g,interpolate:h,variable:"",imports:{_:a}};var ge={a:"q,w,g",h:"var a=arguments,b=0,c=typeof g=='number'?2:a.length;while(++b<c){m=a[b];if(m&&r[typeof m]){",f:"if(typeof u[i]=='undefined')u[i]=m[i]",c:"}}"},ye={a:"e,d,x",h:"d=d&&typeof x=='undefined'?d:p.createCallback(d,x)",b:"typeof n=='number'",f:"if(d(m[i],i,e)===false)return u"},he={h:"if(!r[typeof m])return u;"+ye.h,b:!1};
+V.prototype=a.prototype,ve.argsClass||(W=function(n){return n?Qt.call(n,"callee"):!1});var me=te||function(n){return n?typeof n=="object"&&Zt.call(n)==x:!1},de=L({a:"q",e:"[]",h:"if(!(r[typeof q]))return u",f:"u.push(i)",b:!1}),be=ue?function(n){return et(n)?ve.enumPrototypes&&typeof n=="function"||ve.nonEnumArgs&&n.length&&W(n)?de(n):ue(n):[]}:de,_e=L(ye),we={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},Ce=Z(we),je=L(ge,{h:ge.h.replace(";",";if(c>3&&typeof a[c-2]=='function'){var d=p.createCallback(a[--c-1],a[c--],2);}else if(c>2&&typeof a[c-1]=='function'){d=a[--c];}"),f:"u[i]=d?d(u[i],m[i]):m[i]"}),ke=L(ge),xe=L(ye,he,{i:!1}),Oe=L(ye,he);
+tt(/x/)&&(tt=function(n){return typeof n=="function"&&Zt.call(n)==S});var Ee=Jt?function(n){if(!n||Zt.call(n)!=I||!ve.argsClass&&W(n))return!1;var t=n.valueOf,e=typeof t=="function"&&(e=Jt(t))&&Jt(e);return e?n==e||Jt(n)==e:H(n)}:H;pe&&u&&typeof Xt=="function"&&(Ot=xt(Xt,r));var Se=8==ie(m+"08")?ie:function(n,t){return ie(ut(n)?n.replace(d,""):n,t||0)};return a.after=function(n,t){return 1>n?t():function(){return 1>--n?t.apply(this,arguments):void 0}},a.assign=je,a.at=function(n){var t=-1,e=Gt.apply(zt,le.call(arguments,1)),r=e.length,u=It(r);
+for(ve.unindexedChars&&ut(n)&&(n=n.split(""));++t<r;)u[t]=n[e[t]];return u},a.bind=xt,a.bindAll=function(n){for(var t=1<arguments.length?Gt.apply(zt,le.call(arguments,1)):Y(n),e=-1,r=t.length;++e<r;){var u=t[e];n[u]=xt(n[u],n)}return n},a.bindKey=function(n,t){return z(n,t,le.call(arguments,2),i)},a.compact=function(n){for(var t=-1,e=n?n.length:0,r=[];++t<e;){var u=n[t];u&&r.push(u)}return r},a.compose=function(){var n=arguments;return function(){for(var t=arguments,e=n.length;e--;)t=[n[e].apply(this,t)];
+return t[0]}},a.countBy=function(n,t,e){var r={};return t=a.createCallback(t,e),pt(n,function(n,e,u){e=Tt(t(n,e,u)),Qt.call(r,e)?r[e]++:r[e]=1}),r},a.createCallback=function(n,t,e){if(null==n)return Et;var r=typeof n;if("function"!=r){if("object"!=r)return function(t){return t[n]};var u=be(n);return function(t){for(var e=u.length,r=!1;e--&&(r=nt(t[u[e]],n[u[e]],i)););return r}}return typeof t!="undefined"?1===e?function(e){return n.call(t,e)}:2===e?function(e,r){return n.call(t,e,r)}:4===e?function(e,r,u,a){return n.call(t,e,r,u,a)
+}:function(e,r,u){return n.call(t,e,r,u)}:n},a.debounce=function(n,t,e){function r(){a=f=null,l&&(o=n.apply(i,u))}var u,a,o,i,f,l=!0;if(!0===e)var c=!0,l=!1;else e&&q[typeof e]&&(c=e.leading,l="trailing"in e?e.trailing:l);return function(){return u=arguments,i=this,Vt(f),!a&&c?(a=!0,o=n.apply(i,u)):f=Yt(r,t),o}},a.defaults=ke,a.defer=Ot,a.delay=function(n,t){var r=le.call(arguments,2);return Yt(function(){n.apply(e,r)},t)},a.difference=mt,a.filter=lt,a.flatten=bt,a.forEach=pt,a.forIn=xe,a.forOwn=Oe,a.functions=Y,a.groupBy=function(n,t,e){var r={};
+return t=a.createCallback(t,e),pt(n,function(n,e,u){e=Tt(t(n,e,u)),(Qt.call(r,e)?r[e]:r[e]=[]).push(n)}),r},a.initial=function(n,t,e){if(!n)return[];var r=0,u=n.length;if(typeof t!="number"&&null!=t){var o=u;for(t=a.createCallback(t,e);o--&&t(n[o],o,n);)r++}else r=null==t||e?1:t||r;return J(n,0,oe(ae(0,u-r),u))},a.intersection=function(n){var t=arguments,e=t.length,r={0:{}},u=-1,a=n?n.length:0,o=a>=l,i=[],c=i;n:for(;++u<a;){var p=n[u];if(o)var s=f+p,s=r[0][s]?!(c=r[0][s]):c=r[0][s]=[];if(s||0>_t(c,p)){o&&c.push(p);
+for(var v=e;--v;)if(!(r[v]||(r[v]=R(t[v])))(p))continue n;i.push(p)}}return i},a.invert=Z,a.invoke=function(n,t){var e=le.call(arguments,2),r=-1,u=typeof t=="function",a=n?n.length:0,o=It(typeof a=="number"?a:0);return pt(n,function(n){o[++r]=(u?t:n[t]).apply(n,e)}),o},a.keys=be,a.map=st,a.max=vt,a.memoize=function(n,t){var e={};return function(){var r=f+(t?t.apply(this,arguments):arguments[0]);return Qt.call(e,r)?e[r]:e[r]=n.apply(this,arguments)}},a.merge=at,a.min=function(n,t,e){var r=1/0,u=r;
+if(!t&&me(n)){e=-1;for(var o=n.length;++e<o;){var i=n[e];i<u&&(u=i)}}else t=!t&&ut(n)?T:a.createCallback(t,e),_e(n,function(n,e,a){e=t(n,e,a),e<r&&(r=e,u=n)});return u},a.omit=function(n,t,e){var r=typeof t=="function",u={};if(r)t=a.createCallback(t,e);else var o=Gt.apply(zt,le.call(arguments,1));return xe(n,function(n,e,a){(r?!t(n,e,a):0>_t(o,e))&&(u[e]=n)}),u},a.once=function(n){var t,e;return function(){return t?e:(t=!0,e=n.apply(this,arguments),n=null,e)}},a.pairs=function(n){for(var t=-1,e=be(n),r=e.length,u=It(r);++t<r;){var a=e[t];
+u[t]=[a,n[a]]}return u},a.partial=function(n){return z(n,le.call(arguments,1))},a.partialRight=function(n){return z(n,le.call(arguments,1),null,i)},a.pick=function(n,t,e){var r={};if(typeof t!="function")for(var u=-1,o=Gt.apply(zt,le.call(arguments,1)),i=et(n)?o.length:0;++u<i;){var f=o[u];f in n&&(r[f]=n[f])}else t=a.createCallback(t,e),xe(n,function(n,e,u){t(n,e,u)&&(r[e]=n)});return r},a.pluck=st,a.range=function(n,t,e){n=+n||0,e=+e||1,null==t&&(t=n,n=0);var r=-1;t=ae(0,Ut((t-n)/e));for(var u=It(t);++r<t;)u[r]=n,n+=e;
+return u},a.reject=function(n,t,e){return t=a.createCallback(t,e),lt(n,function(n,e,r){return!t(n,e,r)})},a.rest=wt,a.shuffle=function(n){var t=-1,e=n?n.length:0,r=It(typeof e=="number"?e:0);return pt(n,function(n){var e=Ht(fe()*(++t+1));r[t]=r[e],r[e]=n}),r},a.sortBy=function(n,t,e){var r=-1,u=n?n.length:0,o=It(typeof u=="number"?u:0);for(t=a.createCallback(t,e),pt(n,function(n,e,u){o[++r]={a:t(n,e,u),b:r,c:n}}),u=o.length,o.sort(D);u--;)o[u]=o[u].c;return o},a.tap=function(n,t){return t(n),n},a.throttle=function(n,t,e){function r(){i=null,c&&(f=new Nt,a=n.apply(o,u))
+}var u,a,o,i,f=0,l=!0,c=!0;return!1===e?l=!1:e&&q[typeof e]&&(l="leading"in e?e.leading:l,c="trailing"in e?e.trailing:c),function(){var e=new Nt;!i&&!l&&(f=e);var c=t-(e-f);return u=arguments,o=this,0<c?i||(i=Yt(r,c)):(Vt(i),i=null,f=e,a=n.apply(o,u)),a}},a.times=function(n,t,e){n=-1<(n=+n)?n:0;var r=-1,u=It(n);for(t=a.createCallback(t,e,1);++r<n;)u[r]=t(r);return u},a.toArray=function(n){return n&&typeof n.length=="number"?ve.unindexedChars&&ut(n)?n.split(""):J(n):ot(n)},a.union=function(n){return me(n)||(arguments[0]=n?le.call(n):zt),jt(Gt.apply(zt,arguments))
+},a.uniq=jt,a.unzip=function(n){for(var t=-1,e=n?n.length:0,r=e?vt(st(n,"length")):0,u=It(r);++t<e;)for(var a=-1,o=n[t];++a<r;)(u[a]||(u[a]=It(e)))[t]=o[a];return u},a.values=ot,a.where=lt,a.without=function(n){return mt(n,le.call(arguments,1))},a.wrap=function(n,t){return function(){var e=[n];return Wt.apply(e,arguments),t.apply(this,e)}},a.zip=function(n){for(var t=-1,e=n?vt(st(arguments,"length")):0,r=It(e);++t<e;)r[t]=st(arguments,t);return r},a.zipObject=kt,a.collect=st,a.drop=wt,a.each=pt,a.extend=je,a.methods=Y,a.object=kt,a.select=lt,a.tail=wt,a.unique=jt,St(a),a.clone=X,a.cloneDeep=function(n,t,e){return X(n,!0,t,e)
+},a.contains=it,a.escape=function(n){return null==n?"":Tt(n).replace(_,M)},a.every=ft,a.find=ct,a.findIndex=function(n,t,e){var r=-1,u=n?n.length:0;for(t=a.createCallback(t,e);++r<u;)if(t(n[r],r,n))return r;return-1},a.findKey=function(n,t,e){var r;return t=a.createCallback(t,e),Oe(n,function(n,e,u){return t(n,e,u)?(r=e,!1):void 0}),r},a.has=function(n,t){return n?Qt.call(n,t):!1},a.identity=Et,a.indexOf=_t,a.isArguments=W,a.isArray=me,a.isBoolean=function(n){return!0===n||!1===n||Zt.call(n)==O},a.isDate=function(n){return n?typeof n=="object"&&Zt.call(n)==E:!1
+},a.isElement=function(n){return n?1===n.nodeType:!1},a.isEmpty=function(n){var t=!0;if(!n)return t;var e=Zt.call(n),r=n.length;return e==x||e==N||(ve.argsClass?e==k:W(n))||e==I&&typeof r=="number"&&tt(n.splice)?!r:(Oe(n,function(){return t=!1}),t)},a.isEqual=nt,a.isFinite=function(n){return ee(n)&&!re(parseFloat(n))},a.isFunction=tt,a.isNaN=function(n){return rt(n)&&n!=+n},a.isNull=function(n){return null===n},a.isNumber=rt,a.isObject=et,a.isPlainObject=Ee,a.isRegExp=function(n){return n?q[typeof n]&&Zt.call(n)==P:!1
+},a.isString=ut,a.isUndefined=function(n){return typeof n=="undefined"},a.lastIndexOf=function(n,t,e){var r=n?n.length:0;for(typeof e=="number"&&(r=(0>e?ae(0,r+e):oe(e,r-1))+1);r--;)if(n[r]===t)return r;return-1},a.mixin=St,a.noConflict=function(){return r._=Kt,this},a.parseInt=Se,a.random=function(n,t){return null==n&&null==t&&(t=1),n=+n||0,null==t&&(t=n,n=0),n+Ht(fe()*((+t||0)-n+1))},a.reduce=gt,a.reduceRight=yt,a.result=function(n,t){var r=n?n[t]:e;return tt(r)?n[t]():r},a.runInContext=t,a.size=function(n){var t=n?n.length:0;
+return typeof t=="number"?t:be(n).length},a.some=ht,a.sortedIndex=Ct,a.template=function(n,t,r){var u=a.templateSettings;n||(n=""),r=ke({},r,u);var o,i=ke({},r.imports,u.imports),u=be(i),i=ot(i),f=0,l=r.interpolate||b,v="__p+='",l=Rt((r.escape||b).source+"|"+l.source+"|"+(l===h?g:b).source+"|"+(r.evaluate||b).source+"|$","g");n.replace(l,function(t,e,r,u,a,i){return r||(r=u),v+=n.slice(f,i).replace(w,K),e&&(v+="'+__e("+e+")+'"),a&&(o=!0,v+="';"+a+";__p+='"),r&&(v+="'+((__t=("+r+"))==null?'':__t)+'"),f=i+t.length,t
+}),v+="';\n",l=r=r.variable,l||(r="obj",v="with("+r+"){"+v+"}"),v=(o?v.replace(c,""):v).replace(p,"$1").replace(s,"$1;"),v="function("+r+"){"+(l?"":r+"||("+r+"={});")+"var __t,__p='',__e=_.escape"+(o?",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,'')}":";")+v+"return __p}";try{var y=$t(u,"return "+v).apply(e,i)}catch(m){throw m.source=v,m}return t?y(t):(y.source=v,y)},a.unescape=function(n){return null==n?"":Tt(n).replace(v,Q)},a.uniqueId=function(n){var t=++o;return Tt(null==n?"":n)+t
+},a.all=ft,a.any=ht,a.detect=ct,a.foldl=gt,a.foldr=yt,a.include=it,a.inject=gt,Oe(a,function(n,t){a.prototype[t]||(a.prototype[t]=function(){var t=[this.__wrapped__];return Wt.apply(t,arguments),n.apply(a,t)})}),a.first=dt,a.last=function(n,t,e){if(n){var r=0,u=n.length;if(typeof t!="number"&&null!=t){var o=u;for(t=a.createCallback(t,e);o--&&t(n[o],o,n);)r++}else if(r=t,null==r||e)return n[u-1];return J(n,ae(0,u-r))}},a.take=dt,a.head=dt,Oe(a,function(n,t){a.prototype[t]||(a.prototype[t]=function(t,e){var r=n(this.__wrapped__,t,e);
+return null==t||e&&typeof t!="function"?r:new V(r)})}),a.VERSION="1.2.1",a.prototype.toString=function(){return Tt(this.__wrapped__)},a.prototype.value=At,a.prototype.valueOf=At,_e(["join","pop","shift"],function(n){var t=zt[n];a.prototype[n]=function(){return t.apply(this.__wrapped__,arguments)}}),_e(["push","reverse","sort","unshift"],function(n){var t=zt[n];a.prototype[n]=function(){return t.apply(this.__wrapped__,arguments),this}}),_e(["concat","slice","splice"],function(n){var t=zt[n];a.prototype[n]=function(){return new V(t.apply(this.__wrapped__,arguments))
+}}),ve.spliceObjects||_e(["pop","shift","splice"],function(n){var t=zt[n],e="splice"==n;a.prototype[n]=function(){var n=this.__wrapped__,r=t.apply(n,arguments);return 0===n.length&&delete n[0],e?new V(r):r}}),a}var e,r=typeof exports=="object"&&exports,u=typeof module=="object"&&module&&module.exports==r&&module,a=typeof global=="object"&&global;(a.global===a||a.window===a)&&(n=a);var o=0,i={},f=+new Date+"",l=200,c=/\b__p\+='';/g,p=/\b(__p\+=)''\+/g,s=/(__e\(.*?\)|\b__t\))\+'';/g,v=/&(?:amp|lt|gt|quot|#39);/g,g=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,y=/\w*$/,h=/<%=([\s\S]+?)%>/g,m=" \t\x0B\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000",d=RegExp("^["+m+"]*0+(?=.$)"),b=/($^)/,_=/[&<>"']/g,w=/['\n\r\t\u2028\u2029\\]/g,C="Array Boolean Date Function Math Number Object RegExp String _ attachEvent clearTimeout isFinite isNaN parseInt setImmediate setTimeout".split(" "),j="constructor hasOwnProperty isPrototypeOf propertyIsEnumerable toLocaleString toString valueOf".split(" "),k="[object Arguments]",x="[object Array]",O="[object Boolean]",E="[object Date]",S="[object Function]",A="[object Number]",I="[object Object]",P="[object RegExp]",N="[object String]",$={};
+$[S]=!1,$[k]=$[x]=$[O]=$[E]=$[A]=$[I]=$[P]=$[N]=!0;var q={"boolean":!1,"function":!0,object:!0,number:!1,string:!1,undefined:!1},B={"\\":"\\","'":"'","\n":"n","\r":"r","\t":"t","\u2028":"u2028","\u2029":"u2029"},F=t();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(n._=F,define(function(){return F})):r&&!r.nodeType?u?(u.exports=F)._=F:r._=F:n._=F})(this);
\ No newline at end of file
index 000fbb234398c776dad41cf2e94faf538c4c4fcd..95f323cf257f0c4d0e265a941110f6c0c1de816d 100644 (file)
@@ -12,7 +12,8 @@
             var area = $("#kiwi #controlbox textarea");
 
             for (var i = 0; files.length>i; i++) {
-                area.val(area.val() + " " + files[i].url);
+                var f = files[i];
+                area.val(area.val() + " " + f.url + "+" + encodeURIComponent(f.filename));
             }
         }
 
@@ -23,4 +24,4 @@
 
         control.addPluginIcon($icon);
     });
-</script>
\ No newline at end of file
+</script>
diff --git a/kiwi.bat b/kiwi.bat
new file mode 100755 (executable)
index 0000000..118b58e
--- /dev/null
+++ b/kiwi.bat
@@ -0,0 +1,2 @@
+@echo off\r
+node %~dp0\server\server.js %*
\ No newline at end of file
index 3f212a718b81fd0a76f1042b385cc7bbc9be4322..002b920f532e645cd11a22e56a5dfd4868b302c1 100644 (file)
@@ -20,6 +20,7 @@
     "lodash": "0.9.1",\r
     "daemonize2": "0.4.0-rc.5",\r
     "eventemitter2": "0.4.11",\r
-    "ipaddr.js": "0.1.1"\r
+    "ipaddr.js": "0.1.1",\r
+    "socksjs": "0.3.1"\r
   }\r
 }\r
index 819e06c5a532546e606acd016952563641a5a52b..fb97f5f67cb0af571b7f5e72f6f733a8ab8017de 100644 (file)
@@ -27,7 +27,7 @@ var IrcChannel = function(irc_connection, name) {
         topicsetby:     onTopicSetBy,
         mode:           onMode
     };
-    EventBinder.bindIrcEvents('channel:' + this.name, this.irc_events, this, irc_connection);
+    EventBinder.bindIrcEvents('channel ' + this.name, this.irc_events, this, irc_connection);
 }
 
 
@@ -35,7 +35,7 @@ module.exports = IrcChannel;
 
 
 IrcChannel.prototype.dispose = function (){
-    EventBinder.unbindIrcEvents('channel:' + this.name, this.irc_events, this.irc_connection);
+    EventBinder.unbindIrcEvents('channel ' + this.name, this.irc_events, this.irc_connection);
     this.irc_connection = undefined;
 };
 
@@ -210,29 +210,3 @@ function onMode(event) {
         modes: event.modes
     });
 };
-
-
-/*
-server:event
-server:*
-channel:#channel:event
-channel:*:event
-user:event
-user:*
-
-Server disconnected:
-    server:disconnect
-    server:*
-
-Joining channel #kiwiirc:
-    channel:#kiwiirc:join
-    channel:*:join
-
-Channel message:
-    channel:#kiwiirc:privmsg
-    channel:*:privmsg
-
-Private message:
-    user:privmsg
-    user:*
-*/
\ No newline at end of file
index d37186cb32fc0e60b73ab66fed3ab87af7f3cee8..71b848235b2e533692662909f8c47c78e850a14f 100644 (file)
@@ -96,7 +96,7 @@ var listeners = {
         var nick =  command.params[0];
         this.irc_connection.registered = true;
         this.cap_negotation = false;
-        this.irc_connection.emit('server:' + this.irc_connection.irc_host.hostname + ':connect', {
+        this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' connect', {
             nick: nick
         });
     },
@@ -125,19 +125,19 @@ var listeners = {
                 }
             }
         }
-        this.irc_connection.emit('server:'  + this.irc_connection.irc_host.hostname + ':options', {
+        this.irc_connection.emit('server '  + this.irc_connection.irc_host.hostname + ' options', {
             options: this.irc_connection.options,
             cap: this.irc_connection.cap.enabled
         });
     },
     'RPL_ENDOFWHOIS': function (command) {
-        this.irc_connection.emit('user:' + command.params[1] + ':endofwhois', {
+        this.irc_connection.emit('user ' + command.params[1] + ' endofwhois', {
             nick: command.params[1],
             msg: command.trailing
         });
     },
     'RPL_WHOISUSER': function (command) {
-        this.irc_connection.emit('user:' + command.params[1] + ':whoisuser', {
+        this.irc_connection.emit('user ' + command.params[1] + ' whoisuser', {
             nick: command.params[1],
             ident: command.params[2],
             host: command.params[3],
@@ -145,65 +145,65 @@ var listeners = {
         });
     },
     'RPL_WHOISSERVER': function (command) {
-        this.irc_connection.emit('user:' + command.params[1] + ':whoisserver', {
+        this.irc_connection.emit('user ' + command.params[1] + ' whoisserver', {
             nick: command.params[1],
             irc_server: command.params[2]
         });
     },
     'RPL_WHOISOPERATOR': function (command) {
-        this.irc_connection.emit('user:' + command.params[1] + ':whoisoperator', {
+        this.irc_connection.emit('user ' + command.params[1] + ' whoisoperator', {
             nick: command.params[1],
             msg: command.trailing
         });
     },
     'RPL_WHOISCHANNELS':       function (command) {
-        this.irc_connection.emit('user:' + command.params[1] + ':whoischannels', {
+        this.irc_connection.emit('user ' + command.params[1] + ' whoischannels', {
             nick: command.params[1],
             chans: command.trailing
         });
     },
     'RPL_WHOISMODES': function (command) {
-        this.irc_connection.emit('user:' + command.params[1] + ':whoismodes', {
+        this.irc_connection.emit('user ' + command.params[1] + ' whoismodes', {
             nick: command.params[1],
             msg: command.trailing
         });
     },
     'RPL_WHOISIDLE': function (command) {
-        this.irc_connection.emit('user:' + command.params[1] + ':whoisidle', {
+        this.irc_connection.emit('user ' + command.params[1] + ' whoisidle', {
             nick: command.params[1],
             idle: command.params[2],
             logon: command.params[3] || undefined
         });
     },
     'RPL_WHOISREGNICK': function (command) {
-        this.irc_connection.emit('user:' + command.params[1] + ':whoisregnick', {
+        this.irc_connection.emit('user ' + command.params[1] + ' whoisregnick', {
             nick: command.params[1],
             msg: command.trailing
         });
     },
     'RPL_LISTSTART': function (command) {
-        this.irc_connection.emit('server:' + this.irc_connection.irc_host.hostname + ':list_start', {});
+        this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' list_start', {});
     },
     'RPL_LISTEND': function (command) {
-        this.irc_connection.emit('server:' + this.irc_connection.irc_host.hostname + ':list_end', {});
+        this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' list_end', {});
     },
     'RPL_LIST': function (command) {
-        this.irc_connection.emit('server:' + this.irc_connection.irc_host.hostname + ':list_channel', {
+        this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' list_channel', {
             channel: command.params[1],
             num_users: parseInt(command.params[2], 10),
             topic: command.trailing
         });
     },
     'RPL_MOTD': function (command) {
-        this.irc_connection.emit('server:'  + this.irc_connection.irc_host.hostname + ':motd', {
+        this.irc_connection.emit('server '  + this.irc_connection.irc_host.hostname + ' motd', {
             motd: command.trailing + '\n'
         });
     },
     'RPL_MOTDSTART': function (command) {
-        this.irc_connection.emit('server:'  + this.irc_connection.irc_host.hostname + ':motd_start', {});
+        this.irc_connection.emit('server '  + this.irc_connection.irc_host.hostname + ' motd_start', {});
     },
     'RPL_ENDOFMOTD': function (command) {
-        this.irc_connection.emit('server:'  + this.irc_connection.irc_host.hostname + ':motd_end', {});
+        this.irc_connection.emit('server '  + this.irc_connection.irc_host.hostname + ' motd_end', {});
     },
     'RPL_NAMEREPLY': function (command) {
         var members = command.trailing.split(' ');
@@ -228,7 +228,7 @@ var listeners = {
             member_list.push({nick: member, modes: modes});
         });
 
-        this.irc_connection.emit('channel:' + command.params[2] + ':userlist', {
+        this.irc_connection.emit('channel ' + command.params[2] + ' userlist', {
             users: member_list,
             channel: command.params[2]
         });
@@ -236,14 +236,14 @@ var listeners = {
 
     
     'RPL_ENDOFNAMES': function (command) {
-        this.irc_connection.emit('channel:' + command.params[1] + ':userlist_end', {
+        this.irc_connection.emit('channel ' + command.params[1] + ' userlist_end', {
             channel: command.params[1]
         });
     },
 
 
     'RPL_BANLIST': function (command) {
-        this.irc_connection.emit('channel:' + command.params[1] + ':banlist', {
+        this.irc_connection.emit('channel ' + command.params[1] + ' banlist', {
             channel: command.params[1],
             banned: command.params[2],
             banned_by: command.params[3],
@@ -251,24 +251,24 @@ var listeners = {
         });
     },
     'RPL_ENDOFBANLIST': function (command) {
-        this.irc_connection.emit('channel:' + command.params[1] + ':banlist_end', {
+        this.irc_connection.emit('channel ' + command.params[1] + ' banlist_end', {
             channel: command.params[1]
         });
     },
     'RPL_TOPIC': function (command) {
-        this.irc_connection.emit('channel:' + command.params[1] + ':topic', {
+        this.irc_connection.emit('channel ' + command.params[1] + ' topic', {
             channel: command.params[1],
             topic: command.trailing
         });
     },
     'RPL_NOTOPIC': function (command) {
-        this.irc_connection.emit('channel:' + command.params[1] + ':topic', {
+        this.irc_connection.emit('channel ' + command.params[1] + ' topic', {
             channel: command.params[1],
             topic: ''
         });
     },
     'RPL_TOPICWHOTIME': function (command) {
-        this.irc_connection.emit('channel:' + command.params[1] + ':topicsetby', {
+        this.irc_connection.emit('channel ' + command.params[1] + ' topicsetby', {
             nick: command.params[2],
             channel: command.params[1],
             when: command.params[3]
@@ -287,7 +287,7 @@ var listeners = {
             channel = command.params[0];
         }
         
-        this.irc_connection.emit('channel:' + channel + ':join', {
+        this.irc_connection.emit('channel ' + channel + ' join', {
             nick: command.nick,
             ident: command.ident,
             hostname: command.hostname,
@@ -297,7 +297,7 @@ var listeners = {
 
 
     'PART': function (command) {
-        this.irc_connection.emit('channel:' + command.params[0] + ':part', {
+        this.irc_connection.emit('channel ' + command.params[0] + ' part', {
             nick: command.nick,
             ident: command.ident,
             hostname: command.hostname,
@@ -308,7 +308,7 @@ var listeners = {
 
 
     'KICK': function (command) {
-        this.irc_connection.emit('channel:' + command.params[0] + ':kick', {
+        this.irc_connection.emit('channel ' + command.params[0] + ' kick', {
             kicked: command.params[1],
             nick: command.nick,
             ident: command.ident,
@@ -320,7 +320,7 @@ var listeners = {
 
 
     'QUIT': function (command) {
-        this.irc_connection.emit('user:' + command.nick + ':quit', {
+        this.irc_connection.emit('user ' + command.nick + ' quit', {
             nick: command.nick,
             ident: command.ident,
             hostname: command.hostname,
@@ -335,7 +335,7 @@ var listeners = {
         if ((command.trailing.charAt(0) === String.fromCharCode(1)) && (command.trailing.charAt(command.trailing.length - 1) === String.fromCharCode(1))) {
             // It's a CTCP response
             namespace = (command.params[0] == this.irc_connection.nick) ? 'user' : 'channel';
-            this.irc_connection.emit(namespace + ':' + command.params[0] + ':ctcp_response', {
+            this.irc_connection.emit(namespace + ' ' + command.params[0] + ' ctcp_response', {
                 nick: command.nick,
                 ident: command.ident,
                 hostname: command.hostname,
@@ -344,7 +344,7 @@ var listeners = {
             });
         } else {
             namespace = (command.params[0] == this.irc_connection.nick) ? 'user' : 'channel';
-            this.irc_connection.emit(namespace + ':' + command.params[0] + ':notice', {
+            this.irc_connection.emit(namespace + ' ' + command.params[0] + ' notice', {
                 nick: command.nick,
                 ident: command.ident,
                 hostname: command.hostname,
@@ -354,7 +354,7 @@ var listeners = {
         }
     },
     'NICK': function (command) {
-        this.irc_connection.emit('user:' + command.nick + ':nick', {
+        this.irc_connection.emit('user ' + command.nick + ' nick', {
             nick: command.nick,
             ident: command.ident,
             hostname: command.hostname,
@@ -368,13 +368,13 @@ var listeners = {
         var channel = command.params[0],
             topic = command.trailing || '';
 
-        this.irc_connection.emit('channel:' + channel + ':topic', {
+        this.irc_connection.emit('channel ' + channel + ' topic', {
             nick: command.nick,
             channel: channel,
             topic: topic
         });
     },
-    'MODE': function (command) {                
+    'MODE': function (command) {
         var chanmodes = this.irc_connection.options.CHANMODES || [],
             prefixes = this.irc_connection.options.PREFIX || [],
             always_param = (chanmodes[0] || '').concat((chanmodes[1] || '')),
@@ -423,7 +423,7 @@ var listeners = {
             }
         }
         
-        event = (_.contains(this.irc_connection.options.CHANTYPES, command.params[0][0]) ? 'channel:' : 'user:') + command.params[0] + ':mode';
+        event = (_.contains(this.irc_connection.options.CHANTYPES, command.params[0][0]) ? 'channel ' : 'user ') + command.params[0] + ' mode';
         
         this.irc_connection.emit(event, {
             target: command.params[0],
@@ -449,7 +449,7 @@ var listeners = {
                 this.irc_connection.write('NOTICE ' + command.nick + ' :' + String.fromCharCode(1) + 'CLIENTINFO SOURCE VERSION TIME' + String.fromCharCode(1));
             } else {
                 namespace = (command.params[0].toLowerCase() == this.irc_connection.nick.toLowerCase()) ? 'user' : 'channel';
-                this.irc_connection.emit(namespace + ':' + command.nick + ':ctcp_request', {
+                this.irc_connection.emit(namespace + ' ' + command.nick + ' ctcp_request', {
                     nick: command.nick,
                     ident: command.ident,
                     hostname: command.hostname,
@@ -460,8 +460,8 @@ var listeners = {
             }
         } else {
             // A message to a user (private message) or to a channel?
-            namespace = (command.params[0] === this.irc_connection.nick) ? 'user:' + command.nick : 'channel:' + command.params[0];
-            this.irc_connection.emit(namespace + ':privmsg', {
+            namespace = (command.params[0] === this.irc_connection.nick) ? 'user ' + command.nick : 'channel ' + command.params[0];
+            this.irc_connection.emit(namespace + ' privmsg', {
                 nick: command.nick,
                 ident: command.ident,
                 hostname: command.hostname,
@@ -475,14 +475,17 @@ var listeners = {
         // i.e. - for disable, ~ for requires ACK, = for sticky
         var capabilities = command.trailing.replace(/[\-~=]/, '').split(' ');
         var request;
+
+        // Which capabilities we want to enable
         var want = ['multi-prefix', 'away-notify'];
-        
+
         if (this.irc_connection.password) {
             want.push('sasl');
         }
-        
+
         switch (command.params[1]) {
             case 'LS':
+                // Compute which of the available capabilities we want and request them
                 request = _.intersection(capabilities, want);
                 if (request.length > 0) {
                     this.irc_connection.cap.requested = request;
@@ -494,10 +497,12 @@ var listeners = {
                 break;
             case 'ACK':
                 if (capabilities.length > 0) {
+                    // Update list of enabled capabilities
                     this.irc_connection.cap.enabled = capabilities;
+                    // Update list of capabilities we would like to have but that aren't enabled
                     this.irc_connection.cap.requested = _.difference(this.irc_connection.cap.requested, capabilities);
                 }
-                if (this.irc_connection.cap.requested.length > 0) {
+                if (this.irc_connection.cap.enabled.length > 0) {
                     if (_.contains(this.irc_connection.cap.enabled, 'sasl')) {
                         this.irc_connection.sasl = true;
                         this.irc_connection.write('AUTHENTICATE PLAIN');
@@ -540,7 +545,7 @@ var listeners = {
         }
     },
     'AWAY': function (command) {
-        this.irc_connection.emit('user:' + command.nick + ':away', {
+        this.irc_connection.emit('user ' + command.nick + ' away', {
             nick: command.nick,
             msg: command.trailing
         });
@@ -567,82 +572,82 @@ var listeners = {
         // noop
     },
     'ERROR': function (command) {
-        this.irc_connection.emit('server:' + this.irc_connection.irc_host.hostname + ':error', {
+        this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' error', {
             reason: command.trailing
         });
     },
     ERR_PASSWDMISMATCH: function (command) {
-        this.irc_connection.emit('server:' + this.irc_connection.irc_host.hostname + ':password_mismatch', {});
+        this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' password_mismatch', {});
     },
     ERR_LINKCHANNEL: function (command) {
-        this.irc_connection.emit('server:' + this.irc_connection.irc_host.hostname + ':channel_redirect', {
+        this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' channel_redirect', {
             from: command.params[1],
             to: command.params[2]
         });
     },
     ERR_NOSUCHNICK: function (command) {
-        this.irc_connection.emit('server:' + this.irc_connection.irc_host.hostname + ':no_such_nick', {
+        this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' no_such_nick', {
             nick: command.params[1],
             reason: command.trailing
         });
     },
     ERR_CANNOTSENDTOCHAN: function (command) {
-        this.irc_connection.emit('server:' + this.irc_connection.irc_host.hostname + ':cannot_send_to_chan', {
+        this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' cannot_send_to_chan', {
             channel: command.params[1],
             reason: command.trailing
         });
     },
     ERR_TOOMANYCHANNELS: function (command) {
-        this.irc_connection.emit('server:' + this.irc_connection.irc_host.hostname + ':too_many_channels', {
+        this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' too_many_channels', {
             channel: command.params[1],
             reason: command.trailing
         });
     },
     ERR_USERNOTINCHANNEL: function (command) {
-        this.irc_connection.emit('server:' + this.irc_connection.irc_host.hostname + ':user_not_in_channel', {
+        this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' user_not_in_channel', {
             nick: command.params[0],
             channel: command.params[1],
             reason: command.trailing
         });
     },
     ERR_NOTONCHANNEL: function (command) {
-        this.irc_connection.emit('server:' + this.irc_connection.irc_host.hostname + ':not_on_channel', {
+        this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' not_on_channel', {
             channel: command.params[1],
             reason: command.trailing
         });
     },
     ERR_CHANNELISFULL: function (command) {
-            this.irc_connection.emit('server:' + this.irc_connection.irc_host.hostname + ':channel_is_full', {
+            this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' channel_is_full', {
                 channel: command.params[1],
                 reason: command.trailing
             });
         },
     ERR_INVITEONLYCHAN: function (command) {
-        this.irc_connection.emit('server:' + this.irc_connection.irc_host.hostname + ':invite_only_channel', {
+        this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' invite_only_channel', {
             channel: command.params[1],
             reason: command.trailing
         });
     },
     ERR_BANNEDFROMCHAN: function (command) {
-        this.irc_connection.emit('server:' + this.irc_connection.irc_host.hostname + ':banned_from_channel', {
+        this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' banned_from_channel', {
             channel: command.params[1],
             reason: command.trailing
         });
     },
     ERR_BADCHANNELKEY: function (command) {
-        this.irc_connection.emit('server:' + this.irc_connection.irc_host.hostname + ':bad_channel_key', {
+        this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' bad_channel_key', {
             channel: command.params[1],
             reason: command.trailing
         });
     },
     ERR_CHANOPRIVSNEEDED: function (command) {
-        this.irc_connection.emit('server:' + this.irc_connection.irc_host.hostname + ':chanop_privs_needed', {
+        this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' chanop_privs_needed', {
             channel: command.params[1],
             reason: command.trailing
         });
     },
     ERR_NICKNAMEINUSE: function (command) {
-        this.irc_connection.emit('server:' + this.irc_connection.irc_host.hostname + ':nickname_in_use', {
+        this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' nickname_in_use', {
             nick: command.params[1],
             reason: command.trailing
         });
index c392cac91da2ba51686693dedb3e324703629663..b2ec4dd0bc3650761d7ad061a30562142187c2c4 100644 (file)
@@ -17,7 +17,7 @@ var version_values = process.version.substr(1).split('.').map(function (item) {
 
 // If we have a suitable Nodejs version, bring int he socks functionality
 if (version_values[1] >= 10) {
-    Socks = require('../socks.js');
+    Socks = require('socksjs');
 }
 
 var IrcConnection = function (hostname, port, ssl, nick, user, pass, state) {
@@ -25,7 +25,7 @@ var IrcConnection = function (hostname, port, ssl, nick, user, pass, state) {
 
     EventEmitter2.call(this,{
         wildcard: true,
-        delimiter: ':'
+        delimiter: ' '
     });
     this.setMaxListeners(0);
     
@@ -52,7 +52,10 @@ var IrcConnection = function (hostname, port, ssl, nick, user, pass, state) {
     
     // IrcUser objects
     this.irc_users = Object.create(null);
-    
+
+    // TODO: use `this.nick` instead of `'*'` when using an IrcUser per nick
+    this.irc_users[this.nick] = new IrcUser(this, '*');
+
     // IrcChannel objects
     this.irc_channels = Object.create(null);
 
@@ -87,7 +90,7 @@ var IrcConnection = function (hostname, port, ssl, nick, user, pass, state) {
     this.applyIrcEvents();
 
     // Call any modules before making the connection
-    global.modules.emit('irc:connecting', {connection: this})
+    global.modules.emit('irc connecting', {connection: this})
         .done(function () {
             that.connect();
         });
@@ -101,15 +104,15 @@ module.exports.IrcConnection = IrcConnection;
 IrcConnection.prototype.applyIrcEvents = function () {
     // Listen for events on the IRC connection
     this.irc_events = {
-        'server:*:connect':  onServerConnect,
-        'channel:*:join':    onChannelJoin,
+        'server * connect':  onServerConnect,
+        'channel * join':    onChannelJoin,
 
         // TODO: uncomment when using an IrcUser per nick
         //'user:*:privmsg':    onUserPrivmsg,
-        'user:*:nick':       onUserNick,
-        'channel:*:part':    onUserParts,
-        'channel:*:quit':    onUserParts,
-        'channel:*:kick':    onUserParts
+        'user * nick':       onUserNick,
+        'channel * part':    onUserParts,
+        'channel * quit':    onUserParts,
+        'channel * kick':    onUserKick
     };
 
     EventBinder.bindIrcEvents('', this.irc_events, this, this);
@@ -271,9 +274,6 @@ function onChannelJoin(event) {
 
 function onServerConnect(event) {
     this.nick = event.nick;
-
-    // TODO: use `event.nick` instead of `'*'` when using an IrcUser per nick
-    this.irc_users[event.nick] = new IrcUser(this, '*');
 }
 
 
@@ -314,6 +314,18 @@ function onUserParts(event) {
     }
 }
 
+function onUserKick(event){
+    // Only deal with ourselves being kicked from a channel
+    if (event.kicked !== this.nick)
+        return;
+
+    if (this.irc_channels[event.channel]) {
+        this.irc_channels[event.channel].dispose();
+        delete this.irc_channels[event.channel];
+    }
+
+}
+
 
 
 
@@ -335,7 +347,7 @@ var socketConnectHandler = function () {
     // Let the webirc/etc detection modify any required parameters
     connect_data = findWebIrc.call(this, connect_data);
 
-    global.modules.emit('irc:authorize', connect_data).done(function () {
+    global.modules.emit('irc authorize', connect_data).done(function () {
         // Send any initial data for webirc/etc
         if (connect_data.prepend_data) {
             _.each(connect_data.prepend_data, function(data) {
index aae2d2e9fca15f36660ee089c7edf13edc10df13..e4feb769e40bf926e6358848ef8c50f153ed0383 100644 (file)
@@ -3,7 +3,7 @@ var _ = require('lodash');
 
 module.exports.bindIrcEvents = function (events_scope, event_map, context, irc_connection) {
     var namespace_prefix = events_scope ?
-        events_scope + ':' :
+        events_scope + ' ' :
         '';
 
     // Make sure we have a holder for the bound events
@@ -26,7 +26,7 @@ module.exports.bindIrcEvents = function (events_scope, event_map, context, irc_c
 
 module.exports.unbindIrcEvents = function (events_scope, event_map, irc_connection) {
     var namespace_prefix = events_scope ?
-        events_scope + ':' :
+        events_scope + ' ' :
         '';
 
     // No bound events? Then we have nothing to do
index 540d32464d794984e4b0f458428ef9d7c015f992..2a989ec9efd6a328b252878c46fe15a3253fee95 100755 (executable)
@@ -32,7 +32,7 @@ var IrcServer = function (irc_connection) {
         chanop_privs_needed:    onChanopPrivsNeeded,
         nickname_in_use:        onNicknameInUse
     };
-    EventBinder.bindIrcEvents('server:*', this.irc_events, this, this.irc_connection);
+    EventBinder.bindIrcEvents('server *', this.irc_events, this, this.irc_connection);
     
 
 };
@@ -42,7 +42,7 @@ module.exports = IrcServer;
 
 
 IrcServer.prototype.dispose = function (){
-    EventBinder.unbindIrcEvents('server:*', this.irc_events, this.irc_connection);
+    EventBinder.unbindIrcEvents('server *', this.irc_events, this.irc_connection);
     this.irc_connection = undefined;
 };
 
index 833bf890b3d28122a9fc02a5ab3ecac6e3204e98..69dc0a3c747cf2a12a40cb6601113ab04fea8c9c 100755 (executable)
@@ -22,7 +22,7 @@ var IrcUser = function (irc_connection, nick) {
         ctcp_request:   onCtcpRequest,\r
         mode:           onMode\r
     };\r
-    EventBinder.bindIrcEvents('user:' + this.nick, this.irc_events, this, irc_connection);\r
+    EventBinder.bindIrcEvents('user ' + this.nick, this.irc_events, this, irc_connection);\r
 };\r
 \r
 \r
@@ -30,7 +30,7 @@ module.exports = IrcUser;
 \r
 \r
 IrcUser.prototype.dispose = function () {\r
-    EventBinder.unbindIrcEvents('user:' + this.nick, this.irc_events, this.irc_connection);\r
+    EventBinder.unbindIrcEvents('user ' + this.nick, this.irc_events, this.irc_connection);\r
     this.irc_connection = undefined;\r
 };\r
 \r
@@ -44,9 +44,9 @@ function onNick(event) {
     });\r
 \r
     // TODO: uncomment when using an IrcUser per nick\r
-    //EventBinder.unbindIrcEvents('user:' + this.nick, this.irc_events, irc_connection);\r
+    //EventBinder.unbindIrcEvents('user ' + this.nick, this.irc_events, irc_connection);\r
     //this.nick = event.newnick;\r
-    //EventBinder.bindIrcEvents('user:' + this.nick, this.irc_events, this, irc_connection);\r
+    //EventBinder.bindIrcEvents('user ' + this.nick, this.irc_events, this, irc_connection);\r
 };\r
 \r
 function onAway(event) {\r
index 0dc1d491bea0f6152d64f501562e6e4154c2b529..6e697f75a7b306c780af3cc6f3bd003abb455568 100755 (executable)
@@ -172,7 +172,16 @@ _.each(global.config.servers, function (server) {
         clients.remove(client);
     });
 
-    wl.on('listening', webListenerRunning);
+    wl.on('listening', function () {
+        console.log('Listening on %s:%s %s SSL', server.address, server.port, (server.ssl ? 'with' : 'without'));
+        webListenerRunning();
+    });
+
+    wl.on('error', function (err) {
+        console.log('Error listening on %s:%s: %s', server.address, server.port, err.code);
+        // TODO: This should probably be refactored. ^JA
+        webListenerRunning();
+    });
 });
 
 // Once all the listeners are listening, set the processes UID/GID
diff --git a/server/socks.js b/server/socks.js
deleted file mode 100755 (executable)
index 7784f03..0000000
+++ /dev/null
@@ -1,228 +0,0 @@
-var stream  = require('stream'),
-    util    = require('util'),
-    net     = require('net'),
-    tls     = require('tls'),
-    _       = require('lodash'),
-    ipaddr  = require('ipaddr.js');
-
-var SocksConnection = function (remote_options, socks_options) {
-    var that = this;
-    stream.Duplex.call(this);
-    
-    this.remote_options = _.defaults(remote_options, {
-        host: 'localhost',
-        ssl: false,
-        rejectUnauthorized: false
-    });
-    socks_options = _.defaults(socks_options, {
-        host: 'localhost',
-        port: 1080,
-        user: null,
-        pass: null
-    });
-    
-    this.socksAddress = null;
-    this.socksPort = null;
-    
-    this.socksSocket = net.connect({host: socks_options.host, port: socks_options.port}, socksConnected.bind(this, !(!socks_options.user)));
-    this.socksSocket.once('data', socksAuth.bind(this, {user: socks_options.user, pass: socks_options.pass}));
-    this.socksSocket.on('error', function (err) {
-        that.emit('error', err);
-    });
-    
-    this.outSocket = this.socksSocket;
-};
-
-util.inherits(SocksConnection, stream.Duplex);
-
-SocksConnection.connect = function (remote_options, socks_options, connection_listener) {
-    var socks_connection = new SocksConnection(remote_options, socks_options);
-    if (typeof connection_listener === 'Function') {
-        socks_connection.on('connect', connection_listener);
-    }
-    return socks_connection;
-};
-
-SocksConnection.prototype._read = function () {
-    this.outSocket.resume();
-};
-
-SocksConnection.prototype._write = function (chunk, encoding, callback) {
-    this.outSocket.write(chunk, 'utf8', callback);
-};
-
-SocksConnection.prototype.dispose = function () {
-    this.outSocket.destroy();
-    this.outSocket.removeAllListeners();
-    if (this.outSocket !== this.socksSocket) {
-        this.socksSocket.destroy();
-        this.socksSocket.removeAllListeners();
-    }
-    this.removeAllListeners();
-};
-
-var socksConnected = function (auth) {
-    if (auth) {
-        this.socksSocket.write('\x05\x02\x02\x00'); // SOCKS version 5, supporting two auth methods
-                                                    // username/password and 'no authentication'
-    } else {
-        this.socksSocket.write('\x05\x01\x00');     // SOCKS version 5, only supporting 'no auth' scheme
-    }
-};
-
-var socksAuth = function (auth, data) {
-    var bufs = [];
-    switch (data.readUInt8(1)) {
-    case 255:
-        this.emit('error', 'SOCKS: No acceptable authentication methods');
-        this.socksSocket.destroy();
-        break;
-    case 2:
-        bufs[0] = new Buffer([1]);
-        bufs[1] = new Buffer([Buffer.byteLength(auth.user)]);
-        bufs[2] = new Buffer(auth.user);
-        bufs[3] = new Buffer([Buffer.byteLength(auth.pass)]);
-        bufs[4] = new Buffer(auth.pass);
-        this.socksSocket.write(Buffer.concat(bufs));
-        this.socksSocket.once('data', socksAuthStatus.bind(this));
-        break;
-    default:
-        socksRequest.call(this, this.remote_options.host, this.remote_options.port);
-    }
-};
-
-var socksAuthStatus = function (data) {
-    if (data.readUInt8(1) === 1) {
-        socksRequest.call(this, this.remote_options.host, this.remote_options.port);
-    } else {
-        this.emit('error', 'SOCKS: Authentication failed');
-        this.socksSocket.destroy();
-    }
-};
-
-var socksRequest = function (host, port) {
-    var header, type, hostBuf, portBuf;
-    if (net.isIP(host)) {
-        if (net.isIPv4(host)) {
-            type = new Buffer([1]);
-        } else if (net.isIPv6(host)) {
-            type = new Buffer([4]);
-        }
-        host = new Buffer(ipaddr.parse(host).toByteArray());
-    } else {
-        type = new Buffer([3]);
-        hostBuf = new Buffer(host);
-        hostBuf = Buffer.concat([new Buffer([Buffer.byteLength(host)]), hostBuf]);
-    }
-    header = new Buffer([5, 1, 0]);
-    portBuf = new Buffer(2);
-    portBuf.writeUInt16BE(port, 0);
-    this.socksSocket.write(Buffer.concat([header, type, hostBuf, portBuf]));
-    this.socksSocket.once('data', socksReply.bind(this));
-};
-
-var socksReply = function (data) {
-    var err, port, i, addr_len, addr = '';
-    var status = data.readUInt8(1);
-    if (status === 0) {
-        switch (data.readUInt8(3)) {
-        case 1:
-            for (i = 0; i < 4; i++) {
-                if (i !== 0) {
-                    addr += '.';
-                }
-                addr += data.readUInt8(4 + i);
-            }
-            port = data.readUInt16BE(8);
-            break;
-        case 4:
-            for (i = 0; i < 16; i++) {
-                if (i !== 0) {
-                    addr += ':';
-                }
-                addr += data.readUInt8(4 + i);
-            }
-            port = data.readUInt16BE(20);
-            break;
-        case 3:
-            addr_len = data.readUInt8(4);
-            addr = (data.slice(5, 5 + addr_len)).toString();
-            port = data.readUInt16BE(5 + addr_len);
-        }
-        this.socksAddress = addr;
-        this.socksPort = port;
-        
-        if (this.remote_options.ssl) {
-            startTLS.call(this);
-        } else {
-            proxyData.call(this);
-            this.emit('connect');
-        }
-        
-    } else {
-        switch (status) {
-        case 1:
-            err = 'SOCKS: general SOCKS server failure';
-            break;
-        case 2:
-            err = 'SOCKS: Connection not allowed by ruleset';
-            break;
-        case 3:
-            err = 'SOCKS: Network unreachable';
-            break;
-        case 4:
-            err = 'SOCKS: Host unreachable';
-            break;
-        case 5:
-            err = 'SOCKS: Connection refused';
-            break;
-        case 6:
-            err = 'SOCKS: TTL expired';
-            break;
-        case 7:
-            err = 'SOCKS: Command not supported';
-            break;
-        case 8:
-            err = 'SOCKS: Address type not supported';
-            break;
-        default:
-            err = 'SOCKS: Unknown error';
-        }
-        this.emit('error', err);
-    }
-};
-
-var startTLS = function () {
-    var that = this;
-    var plaintext = tls.connect({
-        socket: this.socksSocket,
-        rejectUnauthorized: this.remote_options.rejectUnauthorized
-    });
-    
-    plaintext.on('error', function (err) {
-        that.emit('error', err);
-    });
-    
-    plaintext.on('secureConnect', function () {
-        that.emit('connect');
-    });
-    this.outSocket = plaintext;
-    proxyData.call(this);
-};
-
-var proxyData = function () {
-    var that = this;
-    
-    this.outSocket.on('data', function (data) {
-        var buffer_not_full = that.push(data);
-        if (!buffer_not_full) {
-            this.pause();
-        }
-    });
-    
-    this.outSocket.on('end', function () {
-        that.push(null);
-    });
-};
-
-module.exports = SocksConnection;
index c67939a2c033abbd687e1a9a98921463fa9b15c1..651d1f69ff424483f3e12e09c99f098b317bc3c3 100644 (file)
@@ -42,15 +42,20 @@ var WebListener = function (web_config, transports) {
 
     if (web_config.ssl) {
         opts = {
-            key: fs.readFileSync(__dirname + '/' + web_config.ssl_key),
-            cert: fs.readFileSync(__dirname + '/' + web_config.ssl_cert)
+            key: fs.readFileSync(web_config.ssl_key),
+            cert: fs.readFileSync(web_config.ssl_cert)
         };
 
         // Do we have an intermediate certificate?
         if (typeof web_config.ssl_ca !== 'undefined') {
-            opts.ca = fs.readFileSync(__dirname + '/' + web_config.ssl_ca);
-        }
+            // An array of them?
+            if (typeof web_config.ssl_ca.map !== 'undefined') {
+                opts.ca = web_config.ssl_ca.map(function (f) { return fs.readFileSync(f); });
 
+            } else {
+                opts.ca = fs.readFileSync(web_config.ssl_ca);
+            }
+        }
 
         hs = https.createServer(opts, handleHttpRequest);
         
@@ -59,8 +64,6 @@ var WebListener = function (web_config, transports) {
         hs.listen(web_config.port, web_config.address, function () {
             that.emit('listening');
         });
-
-        console.log('Listening on ' + web_config.address + ':' + web_config.port.toString() + ' with SSL');
     } else {
 
         // Start some plain-text server up
@@ -71,9 +74,11 @@ var WebListener = function (web_config, transports) {
         hs.listen(web_config.port, web_config.address, function () {
             that.emit('listening');
         });
-
-        console.log('Listening on ' + web_config.address + ':' + web_config.port.toString() + ' without SSL');
     }
+
+    hs.on('error', function (err) {
+        that.emit('error', err);
+    })
     
     this.ws.enable('browser client minification');
     this.ws.enable('browser client etag');
@@ -129,17 +134,23 @@ function authoriseConnection(handshakeData, callback) {
             return callback(null, false);
         }
     }
-        
-    dns.reverse(address, function (err, domains) {
-        if (err || domains.length === 0) {
-            handshakeData.revdns = address;
-        } else {
-            handshakeData.revdns = _.first(domains) || address;
-        }
-        
-        // All is well, authorise the connection
+
+
+    try {
+        dns.reverse(address, function (err, domains) {
+            if (err || domains.length === 0) {
+                handshakeData.revdns = address;
+            } else {
+                handshakeData.revdns = _.first(domains) || address;
+            }
+            
+            // All is well, authorise the connection
+            callback(null, true);
+        });
+    } catch (err) {
+        handshakeData.revdns = address;
         callback(null, true);
-    });
+    }
 }
 
 function newConnection(websocket) {
@@ -155,4 +166,4 @@ function newConnection(websocket) {
 
 
 
-module.exports = WebListener;
\ No newline at end of file
+module.exports = WebListener;
index 2d33755b43f16e8fc99d68e69b6cbfc98ed1d115..30b44952b5adda37666505bbfd22dbd189be8be7 100644 (file)
@@ -3,12 +3,12 @@ var kiwiModules = require('../server/modules');
 var module = new kiwiModules.Module('Example Module');
 
 
-module.on('client:connected', function(event, data) {
+module.on('client connected', function(event, data) {
     console.log('Client connection:', data);
 });
 
 
-module.on('client:commands:msg', function(event, data) {
+module.on('client commands msg', function(event, data) {
     console.log('Client msg:', data.args.target, ': ', data.args.msg);
     data.args.msg += ' - modified!';
 });
\ No newline at end of file