3 // Holds anything kiwi client specific (ie. front, gateway, kiwi.plugs..)
14 * A global container for third party access
15 * Will be used to access a limited subset of kiwi functionality
16 * and data (think: plugins)
24 // Entry point to start the kiwi application
25 start: function (opts
) {
28 kiwi
.app
= new kiwi
.model
.Application(opts
);
30 if (opts
.kiwi_server
) {
31 kiwi
.app
.kiwi_server
= opts
.kiwi_server
;
39 utils
: undefined // Re-usable methods
44 // If within a closure, expose the kiwi globals
45 if (typeof global
!== 'undefined') {
46 global
.kiwi
= kiwi
.global
;
50 kiwi
.model
.Application
= Backbone
.Model
.extend(new (function () {
53 // The auto connect details entered into the server select box
54 var auto_connect_details
= {};
56 /** Instance of kiwi.model.PanelList */
59 /** kiwi.view.Application */
62 /** kiwi.view.StatusMessage */
65 /* Address for the kiwi server */
66 this.kiwi_server
= null;
68 this.initialize = function () {
69 // Update `that` with this new Model object
72 // Best guess at where the kiwi server is
73 this.detectKiwiServer();
76 this.start = function () {
77 // Only debug if set in the querystring
78 if (!getQueryVariable('debug')) {
85 kiwi
.gateway
= new kiwi
.model
.Gateway();
86 this.bindGatewayCommands(kiwi
.gateway
);
88 this.initializeClient();
89 this.view
.barsHide(true);
91 this.panels
.server
.server_login
.bind('server_connect', function (event
) {
92 var server_login
= this;
93 auto_connect_details
= event
;
95 server_login
.networkConnecting();
97 $script(that
.kiwi_server
+ '/socket.io/socket.io.js?ts='+(new Date().getTime()), function () {
102 kiwi
.gateway
.set('kiwi_server', that
.kiwi_server
+ '/kiwi');
103 kiwi
.gateway
.set('nick', event
.nick
);
105 kiwi
.gateway
.connect(event
.server
, event
.port
, event
.ssl
, event
.password
, function () {});
112 function kiwiServerNotFound (e
) {
113 that
.panels
.server
.server_login
.showError();
117 this.detectKiwiServer = function () {
118 // If running from file, default to localhost:7777 by default
119 if (window
.location
.protocol
=== 'file') {
120 this.kiwi_server
= 'http://localhost:7777';
123 // Assume the kiwi server is on the same server
124 var proto
= window
.location
.protocol
=== 'https' ?
128 this.kiwi_server
= proto
+ '://' + window
.location
.host
+ ':' + (window
.location
.port
|| '80');
134 this.initializeClient = function () {
135 this.view
= new kiwi
.view
.Application({model
: this, el
: this.get('container')});
139 * Set the UI components up
141 this.panels
= new kiwi
.model
.PanelList();
143 this.controlbox
= new kiwi
.view
.ControlBox({el
: $('#controlbox')[0]});
144 this.bindControllboxCommands(this.controlbox
);
146 this.topicbar
= new kiwi
.view
.TopicBar({el
: $('#topic')[0]});
148 this.message
= new kiwi
.view
.StatusMessage({el
: $('#status_message')[0]});
151 this.panels
.server
.view
.show();
153 // Rejigg the UI sizes
154 this.view
.doLayout();
156 // Populate the server select box with defaults
157 this.panels
.server
.server_login
.populateFields({
158 nick
: getQueryVariable('nick') || 'kiwi_' + Math
.ceil(Math
.random() * 10000).toString(),
159 server
: getQueryVariable('server') || 'irc.kiwiirc.com',
162 channel
: window
.location
.hash
|| '#test'
168 this.bindGatewayCommands = function (gw
) {
169 gw
.on('onmotd', function (event
) {
170 that
.panels
.server
.addMsg(event
.server
, event
.msg
, 'motd');
174 gw
.on('onconnect', function (event
) {
175 that
.view
.barsShow();
177 if (auto_connect_details
.channel
) {
178 kiwi
.gateway
.join(auto_connect_details
.channel
);
186 gw
.on('disconnect', function (event
) {
187 that
.message
.text('You have been disconnected. Attempting to reconnect..');
190 gw
.on('reconnecting', function (event
) {
191 that
.message
.text('You have been disconnected. Attempting to reconnect again in ' + (event
.delay
/1000) + ' seconds..');
193 gw
.on('connect', function (event
) {
194 if (gw_stat
!== 1) return;
196 that
.message
.text('It\'s OK, you\'re connected again :)', {timeout
: 5000});
202 gw
.on('onjoin', function (event
) {
203 var c
, members
, user
;
204 c
= that
.panels
.getByName(event
.channel
);
206 c
= new kiwi
.model
.Channel({name
: event
.channel
});
210 members
= c
.get('members');
211 if (!members
) return;
213 user
= new kiwi
.model
.Member({nick
: event
.nick
, ident
: event
.ident
, hostname
: event
.hostname
});
215 // TODO: highlight the new channel in some way
219 gw
.on('onpart', function (event
) {
220 var channel
, members
, user
,
223 part_options
.type
= 'part';
224 part_options
.message
= event
.message
|| '';
226 channel
= that
.panels
.getByName(event
.channel
);
227 if (!channel
) return;
229 // If this is us, close the panel
230 if (event
.nick
=== kiwi
.gateway
.get('nick')) {
235 members
= channel
.get('members');
236 if (!members
) return;
238 user
= members
.getByNick(event
.nick
);
241 members
.remove(user
, part_options
);
245 gw
.on('onquit', function (event
) {
249 quit_options
.type
= 'quit';
250 quit_options
.message
= event
.message
|| '';
252 $.each(that
.panels
.models
, function (index
, panel
) {
253 if (!panel
.isChannel()) return;
255 member
= panel
.get('members').getByNick(event
.nick
);
257 panel
.get('members').remove(member
, quit_options
);
263 gw
.on('onkick', function (event
) {
264 var channel
, members
, user
,
267 part_options
.type
= 'kick';
268 part_options
.by
= event
.nick
;
269 part_options
.message
= event
.message
|| '';
271 channel
= that
.panels
.getByName(event
.channel
);
272 if (!channel
) return;
274 members
= channel
.get('members');
275 if (!members
) return;
277 user
= members
.getByNick(event
.kicked
);
280 members
.remove(user
, part_options
);
282 if (event
.kicked
=== kiwi
.gateway
.get('nick')) {
289 gw
.on('onmsg', function (event
) {
291 is_pm
= (event
.channel
== kiwi
.gateway
.get('nick'));
294 // If a panel isn't found for this PM, create one
295 panel
= that
.panels
.getByName(event
.nick
);
297 panel
= new kiwi
.model
.Channel({name
: event
.nick
});
298 that
.panels
.add(panel
);
302 // If a panel isn't found for this channel, reroute to the
304 panel
= that
.panels
.getByName(event
.channel
);
306 panel
= that
.panels
.server
;
310 panel
.addMsg(event
.nick
, event
.msg
);
314 gw
.on('onnotice', function (event
) {
317 // Find a panel for the destination(channel) or who its from
318 panel
= that
.panels
.getByName(event
.target
) || that
.panels
.getByName(event
.nick
);
320 panel
= that
.panels
.server
;
323 panel
.addMsg('[' + (event
.nick
||'') + ']', event
.msg
);
327 gw
.on('onaction', function (event
) {
329 is_pm
= (event
.channel
== kiwi
.gateway
.get('nick'));
332 // If a panel isn't found for this PM, create one
333 panel
= that
.panels
.getByName(event
.nick
);
335 panel
= new kiwi
.model
.Channel({name
: event
.nick
});
336 that
.panels
.add(panel
);
340 // If a panel isn't found for this channel, reroute to the
342 panel
= that
.panels
.getByName(event
.channel
);
344 panel
= that
.panels
.server
;
348 panel
.addMsg('', '* ' + event
.nick
+ ' ' + event
.msg
, 'action');
352 gw
.on('ontopic', function (event
) {
354 c
= that
.panels
.getByName(event
.channel
);
357 // Set the channels topic
358 c
.set('topic', event
.topic
);
360 // If this is the active channel, update the topic bar too
361 if (c
.get('name') === kiwi
.app
.panels
.active
.get('name')) {
362 that
.topicbar
.setCurrentTopic(event
.topic
);
367 gw
.on('ontopicsetby', function (event
) {
369 c
= that
.panels
.getByName(event
.channel
);
372 when
= new Date(event
.when
* 1000).toLocaleString();
373 c
.addMsg('', 'Topic set by ' + event
.nick
+ ' at ' + when
, 'topic');
377 gw
.on('onuserlist', function (event
) {
379 channel
= that
.panels
.getByName(event
.channel
);
381 // If we didn't find a channel for this, may aswell leave
382 if (!channel
) return;
384 channel
.temp_userlist
= channel
.temp_userlist
|| [];
385 _
.each(event
.users
, function (item
) {
386 var user
= new kiwi
.model
.Member({nick
: item
.nick
, modes
: item
.modes
});
387 channel
.temp_userlist
.push(user
);
392 gw
.on('onuserlist_end', function (event
) {
394 channel
= that
.panels
.getByName(event
.channel
);
396 // If we didn't find a channel for this, may aswell leave
397 if (!channel
) return;
399 // Update the members list with the new list
400 channel
.get('members').reset(channel
.temp_userlist
|| []);
402 // Clear the temporary userlist
403 delete channel
.temp_userlist
;
407 gw
.on('onmode', function (event
) {
408 var channel
, members
, member
;
410 if (!event
.channel
) return;
411 channel
= that
.panels
.getByName(event
.channel
);
412 if (!channel
) return;
414 members
= channel
.get('members');
415 if (!members
) return;
417 member
= members
.getByNick(event
.effected_nick
);
420 if (event
.mode
[0] === '+') {
421 member
.addMode(event
.mode
.substr(1));
422 } else if (event
.mode
[0] === '-') {
423 member
.removeMode(event
.mode
.substr(1));
428 gw
.on('onnick', function (event
) {
431 $.each(that
.panels
.models
, function (index
, panel
) {
432 if (!panel
.isChannel()) return;
434 member
= panel
.get('members').getByNick(event
.nick
);
436 member
.set('nick', event
.newnick
);
437 panel
.addMsg('', '== ' + event
.nick
+ ' is now known as ' + event
.newnick
, 'action nick');
446 * Bind to certain commands that may be typed into the control box
448 this.bindControllboxCommands = function (controlbox
) {
449 controlbox
.on('unknown_command', this.unknownCommand
);
451 controlbox
.on('command', this.allCommands
);
452 controlbox
.on('command_msg', this.msgCommand
);
454 controlbox
.on('command_action', this.actionCommand
);
455 controlbox
.on('command_me', this.actionCommand
);
457 controlbox
.on('command_join', this.joinCommand
);
458 controlbox
.on('command_j', this.joinCommand
);
460 controlbox
.on('command_part', this.partCommand
);
461 controlbox
.on('command_p', this.partCommand
);
463 controlbox
.on('command_nick', function (ev
) {
464 kiwi
.gateway
.changeNick(ev
.params
[0]);
467 controlbox
.on('command_query', this.queryCommand
);
468 controlbox
.on('command_q', this.queryCommand
);
470 controlbox
.on('command_topic', this.topicCommand
);
472 controlbox
.on('command_notice', this.noticeCommand
);
474 controlbox
.on('command_css', function (ev
) {
475 var queryString
= '?reload=' + new Date().getTime();
476 $('link[rel="stylesheet"]').each(function () {
477 this.href
= this.href
.replace(/\?.*|$/, queryString
);
482 // A fallback action. Send a raw command to the server
483 this.unknownCommand = function (ev
) {
484 var raw_cmd
= ev
.command
+ ' ' + ev
.params
.join(' ');
485 console
.log('RAW: ' + raw_cmd
);
486 kiwi
.gateway
.raw(raw_cmd
);
489 this.allCommands = function (ev
) {
490 console
.log('allCommands', ev
);
493 this.joinCommand = function (ev
) {
494 var channel
, channel_names
;
496 channel_names
= ev
.params
.join(' ').split(',');
498 $.each(channel_names
, function (index
, channel_name
) {
499 // Trim any whitespace off the name
500 channel_name
= channel_name
.trim();
502 // Check if we have the panel already. If not, create it
503 channel
= that
.panels
.getByName(channel_name
);
505 channel
= new kiwi
.model
.Channel({name
: channel_name
});
506 kiwi
.app
.panels
.add(channel
);
509 kiwi
.gateway
.join(channel_name
);
512 if (channel
) channel
.view
.show();
516 this.queryCommand = function (ev
) {
517 var destination
, panel
;
519 destination
= ev
.params
[0];
521 // Check if we have the panel already. If not, create it
522 panel
= that
.panels
.getByName(destination
);
524 panel
= new kiwi
.model
.Channel({name
: destination
});
525 kiwi
.app
.panels
.add(panel
);
528 if (panel
) panel
.view
.show();
532 this.msgCommand = function (ev
) {
533 var destination
= ev
.params
[0],
534 panel
= that
.panels
.getByName(destination
) || that
.panels
.server
;
538 panel
.addMsg(kiwi
.gateway
.get('nick'), ev
.params
.join(' '));
539 kiwi
.gateway
.privmsg(destination
, ev
.params
.join(' '));
542 this.actionCommand = function (ev
) {
543 if (kiwi
.app
.panels
.active
=== kiwi
.app
.panels
.server
) {
547 var panel
= kiwi
.app
.panels
.active
;
548 panel
.addMsg('', '* ' + kiwi
.gateway
.get('nick') + ' ' + ev
.params
.join(' '), 'action');
549 kiwi
.gateway
.action(panel
.get('name'), ev
.params
.join(' '));
552 this.partCommand = function (ev
) {
553 if (ev
.params
.length
=== 0) {
554 kiwi
.gateway
.part(kiwi
.app
.panels
.active
.get('name'));
556 _
.each(ev
.params
, function (channel
) {
557 kiwi
.gateway
.part(channel
);
560 // TODO: More responsive = close tab now, more accurate = leave until part event
561 //kiwi.app.panels.remove(kiwi.app.panels.active);
564 this.topicCommand = function (ev
) {
567 if (ev
.params
.length
=== 0) return;
569 if (that
.isChannelName(ev
.params
[0])) {
570 channel_name
= ev
.params
[0];
573 channel_name
= kiwi
.app
.panels
.active
.get('name');
576 kiwi
.gateway
.topic(channel_name
, ev
.params
.join(' '));
579 this.noticeCommand = function (ev
) {
582 // Make sure we have a destination and some sort of message
583 if (ev
.params
.length
<= 1) return;
585 destination
= ev
.params
[0];
588 kiwi
.gateway
.notice(destination
, ev
.params
.join(' '));
595 this.isChannelName = function (channel_name
) {
596 var channel_prefix
= kiwi
.gateway
.get('channel_prefix');
598 if (!channel_name
|| !channel_name
.length
) return false;
599 return (channel_prefix
.indexOf(channel_name
[0]) > -1);
605 kiwi
.model
.Gateway
= Backbone
.Model
.extend(new (function () {
610 * The name of the network
616 * The address (URL) of the network
622 * The current nickname
628 * The channel prefix for this network
634 * The user prefixes for channel owner/admin/op/voice etc. on this network
637 user_prefixes
: ['~', '&', '@', '+'],
640 * The URL to the Kiwi server
643 //kiwi_server: '//kiwi'
644 kiwi_server
: 'http://localhost:7778/kiwi'
648 this.initialize = function () {
649 // Update `that` with this new Model object
652 // For ease of access. The socket.io object
653 this.socket
= this.get('socket');
655 // Redundant perhaps? Legacy
656 this.session_id
= '';
663 * Connects to the server
664 * @param {String} host The hostname or IP address of the IRC server to connect to
665 * @param {Number} port The port of the IRC server to connect to
666 * @param {Boolean} ssl Whether or not to connect to the IRC server using SSL
667 * @param {String} password The password to supply to the IRC server during registration
668 * @param {Function} callback A callback function to be invoked once Kiwi's server has connected to the IRC server
670 this.connect = function (host
, port
, ssl
, password
, callback
) {
671 this.socket
= io
.connect(this.get('kiwi_server'), {
672 'try multiple transports': true,
673 'connect timeout': 3000,
674 'max reconnection attempts': 7,
675 'reconnection delay': 2000
677 this.socket
.on('connect_failed', function (reason
) {
678 // TODO: When does this even actually get fired? I can't find a case! ~Darren
679 console
.debug('Unable to connect Socket.IO', reason
);
680 console
.log("kiwi.gateway.socket.on('connect_failed')");
681 //kiwi.front.tabviews.server.addMsg(null, ' ', 'Unable to connect to Kiwi IRC.\n' + reason, 'error');
682 this.socket
.disconnect();
683 this.emit("connect_fail", {reason
: reason
});
686 this.socket
.on('error', function (e
) {
687 this.emit("connect_fail", {reason
: e
});
688 console
.log("kiwi.gateway.socket.on('error')", {reason
: e
});
691 this.socket
.on('connecting', function (transport_type
) {
692 console
.log("kiwi.gateway.socket.on('connecting')");
693 this.emit("connecting");
694 that
.trigger("connecting");
697 this.socket
.on('connect', function () {
698 this.emit('irc connect', that
.get('nick'), host
, port
, ssl
, password
, callback
);
699 that
.trigger('connect', {});
702 this.socket
.on('too_many_connections', function () {
703 this.emit("connect_fail", {reason
: 'too_many_connections'});
706 this.socket
.on('message', this.parse
);
708 this.socket
.on('disconnect', function () {
709 that
.trigger("disconnect", {});
710 console
.log("kiwi.gateway.socket.on('disconnect')");
713 this.socket
.on('close', function () {
714 console
.log("kiwi.gateway.socket.on('close')");
717 this.socket
.on('reconnecting', function (reconnectionDelay
, reconnectionAttempts
) {
718 console
.log("kiwi.gateway.socket.on('reconnecting')");
719 that
.trigger("reconnecting", {delay
: reconnectionDelay
, attempts
: reconnectionAttempts
});
722 this.socket
.on('reconnect_failed', function () {
723 console
.log("kiwi.gateway.socket.on('reconnect_failed')");
748 * Parses the response from the server
750 this.parse = function (item
) {
751 //console.log('gateway event', item);
752 if (item
.event
!== undefined) {
753 that
.trigger('on' + item
.event
, item
);
755 switch (item
.event
) {
757 $.each(item
.options
, function (name
, value
) {
760 // TODO: Check this. Why is it only getting the first char?
761 that
.set('channel_prefix', value
.charAt(0));
764 that
.set('name', value
);
767 that
.set('user_prefixes', value
);
774 that
.set('nick', item
.nick
);
778 if (item
.nick
=== that
.get('nick')) {
779 that
.set('nick', item
.newnick
);
784 if (kiwi.gateway.onSync && kiwi.gateway.syncing) {
785 kiwi.gateway.syncing = false;
786 kiwi.gateway.onSync(item);
792 this.emit('kiwi.' + item
.namespace, item
.data
);
799 * Sends data to the server
801 * @param {Object} data The data to send
802 * @param {Function} callback A callback function
804 this.sendData = function (data
, callback
) {
805 this.socket
.emit('message', {sid
: this.session_id
, data
: JSON
.stringify(data
)}, callback
);
809 * Sends a PRIVMSG message
810 * @param {String} target The target of the message (e.g. a channel or nick)
811 * @param {String} msg The message to send
812 * @param {Function} callback A callback function
814 this.privmsg = function (target
, msg
, callback
) {
823 this.sendData(data
, callback
);
827 * Sends a NOTICE message
828 * @param {String} target The target of the message (e.g. a channel or nick)
829 * @param {String} msg The message to send
830 * @param {Function} callback A callback function
832 this.notice = function (target
, msg
, callback
) {
841 this.sendData(data
, callback
);
845 * Sends a CTCP message
846 * @param {Boolean} request Indicates whether this is a CTCP request (true) or reply (false)
847 * @param {String} type The type of CTCP message, e.g. 'VERSION', 'TIME', 'PING' etc.
848 * @param {String} target The target of the message, e.g a channel or nick
849 * @param {String} params Additional paramaters
850 * @param {Function} callback A callback function
852 this.ctcp = function (request
, type
, target
, params
, callback
) {
863 this.sendData(data
, callback
);
867 * @param {String} target The target of the message (e.g. a channel or nick)
868 * @param {String} msg The message to send
869 * @param {Function} callback A callback function
871 this.action = function (target
, msg
, callback
) {
872 this.ctcp(true, 'ACTION', target
, msg
, callback
);
877 * @param {String} channel The channel to join
878 * @param {String} key The key to the channel
879 * @param {Function} callback A callback function
881 this.join = function (channel
, key
, callback
) {
890 this.sendData(data
, callback
);
895 * @param {String} channel The channel to part
896 * @param {Function} callback A callback function
898 this.part = function (channel
, callback
) {
906 this.sendData(data
, callback
);
910 * Queries or modifies a channell topic
911 * @param {String} channel The channel to query or modify
912 * @param {String} new_topic The new topic to set
913 * @param {Function} callback A callback function
915 this.topic = function (channel
, new_topic
, callback
) {
924 this.sendData(data
, callback
);
928 * Kicks a user from a channel
929 * @param {String} channel The channel to kick the user from
930 * @param {String} nick The nick of the user to kick
931 * @param {String} reason The reason for kicking the user
932 * @param {Function} callback A callback function
934 this.kick = function (channel
, nick
, reason
, callback
) {
944 this.sendData(data
, callback
);
948 * Disconnects us from the server
949 * @param {String} msg The quit message to send to the IRC server
950 * @param {Function} callback A callback function
952 this.quit = function (msg
, callback
) {
961 this.sendData(data
, callback
);
965 * Sends a string unmodified to the IRC server
966 * @param {String} data The data to send to the IRC server
967 * @param {Function} callback A callback function
969 this.raw = function (data
, callback
) {
977 this.sendData(data
, callback
);
981 * Changes our nickname
982 * @param {String} new_nick Our new nickname
983 * @param {Function} callback A callback function
985 this.changeNick = function (new_nick
, callback
) {
993 this.sendData(data
, callback
);
997 * Sends data to a fellow Kiwi IRC user
998 * @param {String} target The nick of the Kiwi IRC user to send to
999 * @param {String} data The data to send
1000 * @param {Function} callback A callback function
1002 this.kiwi = function (target
, data
, callback
) {
1011 this.sendData(data
, callback
);
1016 kiwi
.model
.Member
= Backbone
.Model
.extend({
1017 sortModes: function (modes
) {
1018 return modes
.sort(function (a
, b
) {
1019 var a_idx
, b_idx
, i
;
1020 var user_prefixes
= kiwi
.gateway
.get('user_prefixes');
1022 for (i
= 0; i
< user_prefixes
.length
; i
++) {
1023 if (user_prefixes
[i
].mode
=== a
) {
1027 for (i
= 0; i
< user_prefixes
.length
; i
++) {
1028 if (user_prefixes
[i
].mode
=== b
) {
1032 if (a_idx
< b_idx
) {
1034 } else if (a_idx
> b_idx
) {
1041 initialize: function (attributes
) {
1042 var nick
, modes
, prefix
;
1043 nick
= this.stripPrefix(this.get("nick"));
1045 modes
= this.get("modes");
1046 modes
= modes
|| [];
1047 this.sortModes(modes
);
1048 this.set({"nick": nick
, "modes": modes
, "prefix": this.getPrefix(modes
)}, {silent
: true});
1050 addMode: function (mode
) {
1051 var modes_to_add
= mode
.split(''),
1054 modes
= this.get("modes");
1055 $.each(modes_to_add
, function (index
, item
) {
1059 modes
= this.sortModes(modes
);
1060 this.set({"prefix": this.getPrefix(modes
), "modes": modes
});
1062 removeMode: function (mode
) {
1063 var modes_to_remove
= mode
.split(''),
1066 modes
= this.get("modes");
1067 modes
= _
.reject(modes
, function (m
) {
1068 return (_
.indexOf(modes_to_remove
, m
) !== -1);
1071 this.set({"prefix": this.getPrefix(modes
), "modes": modes
});
1073 getPrefix: function (modes
) {
1075 var user_prefixes
= kiwi
.gateway
.get('user_prefixes');
1077 if (typeof modes
[0] !== 'undefined') {
1078 prefix
= _
.detect(user_prefixes
, function (prefix
) {
1079 return prefix
.mode
=== modes
[0];
1081 prefix
= (prefix
) ? prefix
.symbol
: '';
1085 stripPrefix: function (nick
) {
1086 var tmp
= nick
, i
, j
, k
;
1087 var user_prefixes
= kiwi
.gateway
.get('user_prefixes');
1090 for (j
= 0; j
< nick
.length
; j
++) {
1091 for (k
= 0; k
< user_prefixes
.length
; k
++) {
1092 if (nick
.charAt(j
) === user_prefixes
[k
].symbol
) {
1099 return tmp
.substr(i
);
1101 displayNick: function (full
) {
1102 var display
= this.get('nick');
1105 if (this.get("ident")) {
1106 display
+= ' [' + this.get("ident") + '@' + this.get("hostname") + ']';
1115 kiwi
.model
.MemberList
= Backbone
.Collection
.extend({
1116 model
: kiwi
.model
.Member
,
1117 comparator: function (a
, b
) {
1118 var i
, a_modes
, b_modes
, a_idx
, b_idx
, a_nick
, b_nick
;
1119 var user_prefixes
= kiwi
.gateway
.get('user_prefixes');
1120 a_modes
= a
.get("modes");
1121 b_modes
= b
.get("modes");
1122 // Try to sort by modes first
1123 if (a_modes
.length
> 0) {
1124 // a has modes, but b doesn't so a should appear first
1125 if (b_modes
.length
=== 0) {
1129 // Compare the first (highest) mode
1130 for (i
= 0; i
< user_prefixes
.length
; i
++) {
1131 if (user_prefixes
[i
].mode
=== a_modes
[0]) {
1135 for (i
= 0; i
< user_prefixes
.length
; i
++) {
1136 if (user_prefixes
[i
].mode
=== b_modes
[0]) {
1140 if (a_idx
< b_idx
) {
1142 } else if (a_idx
> b_idx
) {
1145 // If we get to here both a and b have the same highest mode so have to resort to lexicographical sorting
1147 } else if (b_modes
.length
> 0) {
1148 // b has modes but a doesn't so b should appear first
1151 a_nick
= a
.get("nick").toLocaleUpperCase();
1152 b_nick
= b
.get("nick").toLocaleUpperCase();
1153 // Lexicographical sorting
1154 if (a_nick
< b_nick
) {
1156 } else if (a_nick
> b_nick
) {
1159 // This should never happen; both users have the same nick.
1160 console
.log('Something\'s gone wrong somewhere - two users have the same nick!');
1164 initialize: function (options
) {
1165 this.view
= new kiwi
.view
.MemberList({"model": this});
1167 getByNick: function (nick
) {
1168 if (typeof nick
!== 'string') return;
1169 return this.find(function (m
) {
1170 return nick
.toLowerCase() === m
.get('nick').toLowerCase();
1176 kiwi
.model
.Panel
= Backbone
.Model
.extend({
1177 initialize: function (attributes
) {
1178 var name
= this.get("name") || "";
1179 this.view
= new kiwi
.view
.Panel({"model": this, "name": name
});
1183 }, {"silent": true});
1186 addMsg: function (nick
, msg
, type
, opts
) {
1187 var message_obj
, bs
, d
;
1191 // Time defaults to now
1192 if (!opts
|| typeof opts
.time
=== 'undefined') {
1194 opts
.time
= d
.getHours().toString().lpad(2, "0") + ":" + d
.getMinutes().toString().lpad(2, "0") + ":" + d
.getSeconds().toString().lpad(2, "0");
1197 // CSS style defaults to empty string
1198 if (!opts
|| typeof opts
.style
=== 'undefined') {
1202 // Run through the plugins
1203 message_obj
= {"msg": msg
, "time": opts
.time
, "nick": nick
, "chan": this.get("name"), "type": type
, "style": opts
.style
};
1204 //tmp = kiwi.plugs.run('addmsg', message_obj);
1209 // The CSS class (action, topic, notice, etc)
1210 if (typeof message_obj
.type
!== "string") {
1211 message_obj
.type
= '';
1214 // Make sure we don't have NaN or something
1215 if (typeof message_obj
.msg
!== "string") {
1216 message_obj
.msg
= '';
1219 // Update the scrollback
1220 bs
= this.get("scrollback");
1221 bs
.push(message_obj
);
1223 // Keep the scrolback limited
1224 if (bs
.length
> 250) {
1227 this.set({"scrollback": bs
}, {silent
: true});
1229 this.trigger("msg", message_obj
);
1232 close: function () {
1236 var members
= this.get('members');
1239 this.unset('members');
1244 // If closing the active panel, switch to the server panel
1245 if (this.cid
=== kiwi
.app
.panels
.active
.cid
) {
1246 kiwi
.app
.panels
.server
.view
.show();
1250 isChannel: function () {
1251 var channel_prefix
= kiwi
.gateway
.get('channel_prefix'),
1252 this_name
= this.get('name');
1254 if (!this_name
) return false;
1255 return (channel_prefix
.indexOf(this_name
[0]) > -1);
1260 kiwi
.model
.PanelList
= Backbone
.Collection
.extend({
1261 model
: kiwi
.model
.Panel
,
1263 // Holds the active panel
1266 comparator: function (chan
) {
1267 return chan
.get("name");
1269 initialize: function () {
1270 this.view
= new kiwi
.view
.Tabs({"el": $('#toolbar .panellist')[0], "model": this});
1272 // Automatically create a server tab
1273 this.add(new kiwi
.model
.Server({'name': kiwi
.gateway
.get('name')}));
1274 this.server
= this.getByName(kiwi
.gateway
.get('name'));
1276 // Keep a tab on the active panel
1277 this.bind('active', function (active_panel
) {
1278 this.active
= active_panel
;
1282 getByName: function (name
) {
1283 if (typeof name
!== 'string') return;
1284 return this.find(function (c
) {
1285 return name
.toLowerCase() === c
.get('name').toLowerCase();
1291 // TODO: Channel modes
1292 // TODO: Listen to gateway events for anythign related to this channel
1293 kiwi
.model
.Channel
= kiwi
.model
.Panel
.extend({
1294 initialize: function (attributes
) {
1295 var name
= this.get("name") || "",
1298 this.view
= new kiwi
.view
.Channel({"model": this, "name": name
});
1300 "members": new kiwi
.model
.MemberList(),
1304 }, {"silent": true});
1306 members
= this.get("members");
1307 members
.bind("add", function (member
) {
1308 this.addMsg(' ', '--> ' + member
.displayNick(true) + ' has joined', 'action join');
1311 members
.bind("remove", function (member
, members
, options
) {
1312 var msg
= (options
.message
) ? '(' + options
.message
+ ')' : '';
1314 if (options
.type
=== 'quit') {
1315 this.addMsg(' ', '<-- ' + member
.displayNick(true) + ' has quit ' + msg
, 'action quit');
1316 } else if(options
.type
=== 'kick') {
1317 this.addMsg(' ', '<-- ' + member
.displayNick(true) + ' was kicked by ' + options
.by
+ ' ' + msg
, 'action kick');
1319 this.addMsg(' ', '<-- ' + member
.displayNick(true) + ' has left ' + msg
, 'action part');
1326 kiwi
.model
.Server
= kiwi
.model
.Panel
.extend({
1329 initialize: function (attributes
) {
1330 var name
= "Server";
1331 this.view
= new kiwi
.view
.Panel({"model": this, "name": name
});
1335 }, {"silent": true});
1337 //this.addMsg(' ', '--> Kiwi IRC: Such an awesome IRC client', '', {style: 'color:#009900;'});
1339 this.server_login
= new kiwi
.view
.ServerSelect();
1341 this.view
.$el
.append(this.server_login
.$el
);
1342 this.server_login
.show();
1347 /*jslint devel: true, browser: true, continue: true, sloppy: true, forin: true, plusplus: true, maxerr: 50, indent: 4, nomen: true, regexp: true*/
1348 /*globals $, front, gateway, Utilityview */
1353 * Suppresses console.log
1354 * @param {Boolean} debug Whether to re-enable console.log or not
1356 function manageDebug(debug
) {
1357 var log
, consoleBackUp
;
1358 if (window
.console
) {
1359 consoleBackUp
= window
.console
.log
;
1360 window
.console
.log = function () {
1362 consoleBackUp
.apply(console
, arguments
);
1366 log
= window
.opera
? window
.opera
.postError
: alert
;
1367 window
.console
= {};
1368 window
.console
.log = function (str
) {
1377 * Generate a random string of given length
1378 * @param {Number} string_length The length of the random string
1379 * @returns {String} The random string
1381 function randomString(string_length
) {
1382 var chars
= "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz",
1386 for (i
= 0; i
< string_length
; i
++) {
1387 rnum
= Math
.floor(Math
.random() * chars
.length
);
1388 randomstring
+= chars
.substring(rnum
, rnum
+ 1);
1390 return randomstring
;
1396 if (typeof String
.prototype.trim
=== 'undefined') {
1397 String
.prototype.trim = function () {
1398 return this.replace(/^\s+|\s+$/g, "");
1404 * @param {Number} length The length of padding
1405 * @param {String} characher The character to pad with
1406 * @returns {String} The padded string
1408 if (typeof String
.prototype.lpad
=== 'undefined') {
1409 String
.prototype.lpad = function (length
, character
) {
1412 for (i
= 0; i
< length
; i
++) {
1413 padding
+= character
;
1415 return (padding
+ this).slice(-length
);
1421 * Convert seconds into hours:minutes:seconds
1422 * @param {Number} secs The number of seconds to converts
1423 * @returns {Object} An object representing the hours/minutes/second conversion of secs
1425 function secondsToTime(secs
) {
1426 var hours
, minutes
, seconds
, divisor_for_minutes
, divisor_for_seconds
, obj
;
1427 hours
= Math
.floor(secs
/ (60 * 60));
1429 divisor_for_minutes
= secs
% (60 * 60);
1430 minutes
= Math
.floor(divisor_for_minutes
/ 60);
1432 divisor_for_seconds
= divisor_for_minutes
% 60;
1433 seconds
= Math
.ceil(divisor_for_seconds
);
1447 * Convert HSL to RGB formatted colour
1449 function hsl2rgb(h
, s
, l
) {
1455 r
= g
= b
= (l
* 255);
1457 function HueToRgb(m1
, m2
, hue
) {
1465 v
= m1
+ (m2
- m1
) * hue
* 6;
1466 else if (2 * hue
< 1)
1468 else if (3 * hue
< 2)
1469 v
= m1
+ (m2
- m1
) * (2/3 - hue
) * 6;
1481 r
= HueToRgb(m1
, m2
, hue
+ 1/3);
1482 g
= HueToRgb(m1
, m2
, hue
);
1483 b
= HueToRgb(m1
, m2
, hue
- 1/3);
1493 * Formats a message. Adds bold, underline and colouring
1494 * @param {String} msg The message to format
1495 * @returns {String} The HTML formatted message
1497 function formatIRCMsg (msg
) {
1500 if ((!msg
) || (typeof msg
!== 'string')) {
1505 if (msg
.indexOf(String
.fromCharCode(2)) !== -1) {
1507 while (msg
.indexOf(String
.fromCharCode(2)) !== -1) {
1508 msg
= msg
.replace(String
.fromCharCode(2), next
);
1509 next
= (next
=== '<b>') ? '</b>' : '<b>';
1511 if (next
=== '</b>') {
1517 if (msg
.indexOf(String
.fromCharCode(31)) !== -1) {
1519 while (msg
.indexOf(String
.fromCharCode(31)) !== -1) {
1520 msg
= msg
.replace(String
.fromCharCode(31), next
);
1521 next
= (next
=== '<u>') ? '</u>' : '<u>';
1523 if (next
=== '</u>') {
1532 msg
= (function (msg
) {
1533 var replace
, colourMatch
, col
, i
, match
, to
, endCol
, fg
, bg
, str
;
1538 colourMatch = function (str
) {
1539 var re
= /^\x03([0-9][0-5]?)(,([0-9][0-5]?))?/;
1540 return re
.exec(str
);
1545 col = function (num
) {
1546 switch (parseInt(num
, 10)) {
1583 if (msg
.indexOf('\x03') !== -1) {
1584 i
= msg
.indexOf('\x03');
1585 replace
= msg
.substr(0, i
);
1586 while (i
< msg
.length
) {
1590 match
= colourMatch(msg
.substr(i
, 6));
1592 //console.log(match);
1594 to
= msg
.indexOf('\x03', i
+ 1);
1595 endCol
= msg
.indexOf(String
.fromCharCode(15), i
+ 1);
1596 if (endCol
!== -1) {
1600 to
= ((to
< endCol
) ? to
: endCol
);
1606 //console.log(i, to);
1609 str
= msg
.substring(i
+ 1 + match
[1].length
+ ((bg
!== null) ? match
[2].length
+ 1 : 0), to
);
1611 replace
+= '<span style="' + ((fg
!== null) ? 'color: ' + fg
+ '; ' : '') + ((bg
!== null) ? 'background-color: ' + bg
+ ';' : '') + '">' + str
+ '</span>';
1614 if ((msg
[i
] !== '\x03') && (msg
[i
] !== String
.fromCharCode(15))) {
1637 Each function in each object is looped through and ran. The resulting text
1638 is expected to be returned.
1643 onaddmsg: function (event
, opts
) {
1648 event
.msg
= event
.msg
.replace(/^((https?\:\/\/|ftp\:\/\/)|(www\.))(\S+)(\w{2,4})(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?(\.jpg|\.jpeg|\.gif|\.bmp|\.png)$/gi, function (url
) {
1649 // Don't let any future plugins change it (ie. html_safe plugins)
1650 event
.event_bubbles
= false;
1652 var img
= '<img class="link_img_a" src="' + url
+ '" height="100%" width="100%" />';
1653 return '<a class="link_ext link_img" target="_blank" rel="nofollow" href="' + url
+ '" style="height:50px;width:50px;display:block">' + img
+ '<div class="tt box"></div></a>';
1662 onaddmsg: function (event
, opts
) {
1663 event
.msg
= $('<div/>').text(event
.msg
).html();
1664 event
.nick
= $('<div/>').text(event
.nick
).html();
1672 onaddmsg: function (event
, opts
) {
1673 //if (kiwi.front.cur_channel.name.toLowerCase() !== kiwi.front.tabviews[event.tabview.toLowerCase()].name) {
1674 // kiwi.front.tabviews[event.tabview].activity();
1683 onaddmsg: function (event
, opts
) {
1684 //var tab = Tabviews.getTab(event.tabview.toLowerCase());
1686 // If we have a highlight...
1687 //if (event.msg.toLowerCase().indexOf(kiwi.gateway.nick.toLowerCase()) > -1) {
1688 // if (Tabview.getCurrentTab() !== tab) {
1691 // if (kiwi.front.isChannel(tab.name)) {
1692 // event.msg = '<span style="color:red;">' + event.msg + '</span>';
1696 // If it's a PM, highlight
1697 //if (!kiwi.front.isChannel(tab.name) && tab.name !== "server"
1698 // && Tabview.getCurrentTab().name.toLowerCase() !== tab.name
1710 //Following method taken from: http://snipplr.com/view/13533/convert-text-urls-into-links/
1711 name
: "linkify_plain",
1712 onaddmsg: function (event
, opts
) {
1717 event
.msg
= event
.msg
.replace(/((https?\:\/\/|ftp\:\/\/)|(www\.))(\S+)(\w{2,4})(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/gi, function (url
) {
1719 // If it's any of the supported images in the images plugin, skip it
1720 if (url
.match(/(\.jpg|\.jpeg|\.gif|\.bmp|\.png)$/)) {
1725 if (url
.match('^https?:\/\/')) {
1726 //nice = nice.replace(/^https?:\/\//i,'')
1727 nice
= url
; // Shutting up JSLint...
1729 url
= 'http://' + url
;
1732 //return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url + '">' + nice + '<div class="tt box"></div></a>';
1733 return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url
+ '">' + nice
+ '</a>';
1742 onaddmsg: function (event
, opts
) {
1747 event
.msg
= event
.msg
.replace(/\n/gi, function (txt
) {
1757 * Disabled due to many websites closing kiwi with iframe busting
1760 oninit: function (event, opts) {
1761 $('#windows a.link_ext').live('mouseover', this.mouseover);
1762 $('#windows a.link_ext').live('mouseout', this.mouseout);
1763 $('#windows a.link_ext').live('click', this.mouseclick);
1766 onunload: function (event, opts) {
1767 // TODO: make this work (remove all .link_ext_browser as created in mouseover())
1768 $('#windows a.link_ext').die('mouseover', this.mouseover);
1769 $('#windows a.link_ext').die('mouseout', this.mouseout);
1770 $('#windows a.link_ext').die('click', this.mouseclick);
1775 mouseover: function (e) {
1780 if (tt.text() === '') {
1781 tooltip = $('<a class="link_ext_browser">Open in Kiwi..</a>');
1785 tt.css('top', -tt.outerHeight() + 'px');
1786 tt.css('left', (a.outerWidth() / 2) - (tt.outerWidth() / 2));
1789 mouseout: function (e) {
1794 mouseclick: function (e) {
1798 switch (e.target.className) {
1803 case 'link_ext_browser':
1804 t = new Utilityview('Browser');
1805 t.topic = a.attr('href');
1807 t.iframe = $('<iframe border="0" class="utility_view" src="" style="width:100%;height:100%;border:none;"></iframe>');
1808 t.iframe.attr('src', a.attr('href'));
1809 t.div.append(t.iframe);
1820 name
: "nick_colour",
1821 onaddmsg: function (event
, opts
) {
1826 //if (typeof kiwi.front.tabviews[event.tabview].nick_colours === 'undefined') {
1827 // kiwi.front.tabviews[event.tabview].nick_colours = {};
1830 //if (typeof kiwi.front.tabviews[event.tabview].nick_colours[event.nick] === 'undefined') {
1831 // kiwi.front.tabviews[event.tabview].nick_colours[event.nick] = this.randColour();
1834 //var c = kiwi.front.tabviews[event.tabview].nick_colours[event.nick];
1835 var c
= this.randColour();
1836 event
.nick
= '<span style="color:' + c
+ ';">' + event
.nick
+ '</span>';
1843 randColour: function () {
1844 var h
= this.rand(-250, 0),
1845 s
= this.rand(30, 100),
1846 l
= this.rand(20, 70);
1847 return 'hsl(' + h
+ ',' + s
+ '%,' + l
+ '%)';
1851 rand: function (min
, max
) {
1852 return parseInt(Math
.random() * (max
- min
+ 1), 10) + min
;
1858 oninit: function (event
, opts
) {
1859 console
.log('registering namespace');
1860 $(gateway
).bind("kiwi.lol.browser", function (e
, data
) {
1861 console
.log('YAY kiwitest');
1881 kiwi
.plugs
.loaded
= {};
1884 * @param {Object} plugin The plugin to be loaded
1885 * @returns {Boolean} True on success, false on failure
1887 kiwi
.plugs
.loadPlugin = function (plugin
) {
1889 if (typeof plugin
.name
!== 'string') {
1893 plugin_ret
= kiwi
.plugs
.run('plugin_load', {plugin
: plugin
});
1894 if (typeof plugin_ret
=== 'object') {
1895 kiwi
.plugs
.loaded
[plugin_ret
.plugin
.name
] = plugin_ret
.plugin
;
1896 kiwi
.plugs
.loaded
[plugin_ret
.plugin
.name
].local_data
= new kiwi
.dataStore('kiwi_plugin_' + plugin_ret
.plugin
.name
);
1898 kiwi
.plugs
.run('init', {}, {run_only
: plugin_ret
.plugin
.name
});
1905 * @param {String} plugin_name The name of the plugin to unload
1907 kiwi
.plugs
.unloadPlugin = function (plugin_name
) {
1908 if (typeof kiwi
.plugs
.loaded
[plugin_name
] !== 'object') {
1912 kiwi
.plugs
.run('unload', {}, {run_only
: plugin_name
});
1913 delete kiwi
.plugs
.loaded
[plugin_name
];
1919 * Run an event against all loaded plugins
1920 * @param {String} event_name The name of the event
1921 * @param {Object} event_data The data to pass to the plugin
1922 * @param {Object} opts Options
1923 * @returns {Object} Event data, possibly modified by the plugins
1925 kiwi
.plugs
.run = function (event_name
, event_data
, opts
) {
1926 var ret
= event_data
,
1930 // Set some defaults if not provided
1931 event_data
= (typeof event_data
=== 'undefined') ? {} : event_data
;
1932 opts
= (typeof opts
=== 'undefined') ? {} : opts
;
1934 for (plugin_name
in kiwi
.plugs
.loaded
) {
1935 // If we're only calling 1 plugin, make sure it's that one
1936 if (typeof opts
.run_only
=== 'string' && opts
.run_only
!== plugin_name
) {
1940 if (typeof kiwi
.plugs
.loaded
[plugin_name
]['on' + event_name
] === 'function') {
1942 ret_tmp
= kiwi
.plugs
.loaded
[plugin_name
]['on' + event_name
](ret
, opts
);
1943 if (ret_tmp
=== null) {
1948 if (typeof ret
.event_bubbles
=== 'boolean' && ret
.event_bubbles
=== false) {
1949 delete ret
.event_bubbles
;
1964 * @param {String} data_namespace The namespace for the data store
1966 kiwi
.dataStore = function (data_namespace
) {
1967 var namespace = data_namespace
;
1969 this.get = function (key
) {
1970 return $.jStorage
.get(data_namespace
+ '_' + key
);
1973 this.set = function (key
, value
) {
1974 return $.jStorage
.set(data_namespace
+ '_' + key
, value
);
1978 kiwi
.data
= new kiwi
.dataStore('kiwi');
1984 * jQuery jStorage plugin
1985 * https://github.com/andris9/jStorage/
1987 (function(f
){if(!f
||!(f
.toJSON
||Object
.toJSON
||window
.JSON
)){throw new Error("jQuery, MooTools or Prototype needs to be loaded before jStorage!")}var g
={},d
={jStorage
:"{}"},h
=null,j
=0,l
=f
.toJSON
||Object
.toJSON
||(window
.JSON
&&(JSON
.encode
||JSON
.stringify
)),e
=f
.evalJSON
||(window
.JSON
&&(JSON
.decode
||JSON
.parse
))||function(m
){return String(m
).evalJSON()},i
=false;_XMLService
={isXML:function(n
){var m
=(n
?n
.ownerDocument
||n
:0).documentElement
;return m
?m
.nodeName
!=="HTML":false},encode:function(n
){if(!this.isXML(n
)){return false}try{return new XMLSerializer().serializeToString(n
)}catch(m
){try{return n
.xml
}catch(o
){}}return false},decode:function(n
){var m
=("DOMParser" in window
&&(new DOMParser()).parseFromString
)||(window
.ActiveXObject
&&function(p
){var q
=new ActiveXObject("Microsoft.XMLDOM");q
.async
="false";q
.loadXML(p
);return q
}),o
;if(!m
){return false}o
=m
.call("DOMParser" in window
&&(new DOMParser())||window
,n
,"text/xml");return this.isXML(o
)?o
:false}};function k(){if("localStorage" in window
){try{if(window
.localStorage
){d
=window
.localStorage
;i
="localStorage"}}catch(p
){}}else{if("globalStorage" in window
){try{if(window
.globalStorage
){d
=window
.globalStorage
[window
.location
.hostname
];i
="globalStorage"}}catch(o
){}}else{h
=document
.createElement("link");if(h
.addBehavior
){h
.style
.behavior
="url(#default#userData)";document
.getElementsByTagName("head")[0].appendChild(h
);h
.load("jStorage");var n
="{}";try{n
=h
.getAttribute("jStorage")}catch(m
){}d
.jStorage
=n
;i
="userDataBehavior"}else{h
=null;return}}}b()}function b(){if(d
.jStorage
){try{g
=e(String(d
.jStorage
))}catch(m
){d
.jStorage
="{}"}}else{d
.jStorage
="{}"}j
=d
.jStorage
?String(d
.jStorage
).length
:0}function c(){try{d
.jStorage
=l(g
);if(h
){h
.setAttribute("jStorage",d
.jStorage
);h
.save("jStorage")}j
=d
.jStorage
?String(d
.jStorage
).length
:0}catch(m
){}}function a(m
){if(!m
||(typeof m
!="string"&&typeof m
!="number")){throw new TypeError("Key name must be string or numeric")}return true}f
.jStorage
={version
:"0.1.5.1",set:function(m
,n
){a(m
);if(_XMLService
.isXML(n
)){n
={_is_xml
:true,xml
:_XMLService
.encode(n
)}}g
[m
]=n
;c();return n
},get:function(m
,n
){a(m
);if(m
in g
){if(g
[m
]&&typeof g
[m
]=="object"&&g
[m
]._is_xml
&&g
[m
]._is_xml
){return _XMLService
.decode(g
[m
].xml
)}else{return g
[m
]}}return typeof(n
)=="undefined"?null:n
},deleteKey:function(m
){a(m
);if(m
in g
){delete g
[m
];c();return true}return false},flush:function(){g
={};c();return true},storageObj:function(){function m(){}m
.prototype=g
;return new m()},index:function(){var m
=[],n
;for(n
in g
){if(g
.hasOwnProperty(n
)){m
.push(n
)}}return m
},storageSize:function(){return j
},currentBackend:function(){return i
},storageAvailable:function(){return !!i
},reInit:function(){var m
,o
;if(h
&&h
.addBehavior
){m
=document
.createElement("link");h
.parentNode
.replaceChild(m
,h
);h
=m
;h
.style
.behavior
="url(#default#userData)";document
.getElementsByTagName("head")[0].appendChild(h
);h
.load("jStorage");o
="{}";try{o
=h
.getAttribute("jStorage")}catch(n
){}d
.jStorage
=o
;i
="userDataBehavior"}b()}};k()})(window
.jQuery
||window
.$);
1990 /*jslint white:true, regexp: true, nomen: true, devel: true, undef: true, browser: true, continue: true, sloppy: true, forin: true, newcap: true, plusplus: true, maxerr: 50, indent: 4 */
1993 kiwi
.view
.MemberList
= Backbone
.View
.extend({
1996 "click .nick": "nickClick"
1998 initialize: function (options
) {
1999 this.model
.bind('all', this.render
, this);
2000 $(this.el
).appendTo('#memberlists');
2002 render: function () {
2003 var $this = $(this.el
);
2005 this.model
.forEach(function (member
) {
2006 $('<li><a class="nick"><span class="prefix">' + member
.get("prefix") + '</span>' + member
.get("nick") + '</a></li>')
2008 .data('member', member
);
2011 nickClick: function (x
) {
2012 var target
= $(x
.currentTarget
).parent('li'),
2013 member
= target
.data('member'),
2014 userbox
= new kiwi
.view
.UserBox();
2016 userbox
.member
= member
;
2017 $('.userbox', this.$el
).remove();
2018 target
.append(userbox
.$el
);
2021 $('#memberlists').children().removeClass('active');
2022 $(this.el
).addClass('active');
2027 kiwi
.view
.UserBox
= Backbone
.View
.extend({
2028 // Member this userbox is relating to
2032 'click .query': 'queryClick',
2033 'click .info': 'infoClick'
2036 initialize: function () {
2037 this.$el
= $($('#tmpl_userbox').html());
2040 queryClick: function (event
) {
2041 var panel
= new kiwi
.model
.Channel({name
: this.member
.get('nick')});
2042 kiwi
.app
.panels
.add(panel
);
2046 infoClick: function (event
) {
2047 kiwi
.gateway
.raw('WHOIS ' + this.member
.get('nick'));
2052 kiwi
.view
.ServerSelect
= Backbone
.View
.extend({
2054 'submit form': 'submitLogin',
2055 'click .show_more': 'showMore'
2058 initialize: function () {
2059 this.$el
= $($('#tmpl_server_select').html());
2061 kiwi
.gateway
.bind('onconnect', this.networkConnected
, this);
2062 kiwi
.gateway
.bind('connecting', this.networkConnecting
, this);
2065 submitLogin: function (event
) {
2067 nick
: $('.nick', this.$el
).val(),
2068 server
: $('.server', this.$el
).val(),
2069 port
: $('.port', this.$el
).val(),
2070 ssl
: $('.ssl', this.$el
).prop('checked'),
2071 password
: $('.password', this.$el
).val(),
2072 channel
: $('.channel', this.$el
).val()
2075 this.trigger('server_connect', values
);
2079 showMore: function (event
) {
2080 $('.more', this.$el
).slideDown('fast');
2083 populateFields: function (defaults
) {
2084 var nick
, server
, channel
;
2086 defaults
= defaults
|| {};
2088 nick
= defaults
.nick
|| '';
2089 server
= defaults
.server
|| '';
2090 port
= defaults
.port
|| 6667;
2091 ssl
= defaults
.ssl
|| 0;
2092 password
= defaults
.password
|| '';
2093 channel
= defaults
.channel
|| '';
2095 $('.nick', this.$el
).val(nick
);
2096 $('.server', this.$el
).val(server
);
2097 $('.port', this.$el
).val(port
);
2098 $('.ssl', this.$el
).prop('checked', ssl
);
2099 $('.password', this.$el
).val(password
);
2100 $('.channel', this.$el
).val(channel
);
2109 $('.nick', this.$el
).focus();
2112 setStatus: function (text
, class_name
) {
2113 $('.status', this.$el
)
2115 .attr('class', 'status')
2116 .addClass(class_name
)
2119 clearStatus: function () {
2120 $('.status', this.$el
).hide();
2123 networkConnected: function (event
) {
2124 this.setStatus('Connected :)', 'ok');
2125 $('form', this.$el
).hide();
2128 networkConnecting: function (event
) {
2129 this.setStatus('Connecting..', 'ok');
2132 showError: function (event
) {
2133 this.setStatus('Error connecting', 'error');
2134 $('form', this.$el
).show();
2139 kiwi
.view
.Panel
= Backbone
.View
.extend({
2141 className
: "messages",
2143 "click .chan": "chanClick"
2146 // The container this panel is within
2149 initialize: function (options
) {
2150 this.initializePanel(options
);
2153 initializePanel: function (options
) {
2154 this.$el
.css('display', 'none');
2156 // Containing element for this panel
2157 if (options
.container
) {
2158 this.$container
= $(options
.container
);
2160 this.$container
= $('#panels .container1');
2163 this.$el
.appendTo(this.$container
);
2165 this.model
.bind('msg', this.newMsg
, this);
2168 this.model
.set({"view": this}, {"silent": true});
2171 render: function () {
2173 this.model
.get("backscroll").forEach(this.newMsg
);
2175 newMsg: function (msg
) {
2176 // TODO: make sure that the message pane is scrolled to the bottom (Or do we? ~Darren)
2177 var re
, line_msg
, $this = this.$el
,
2180 // Escape any HTML that may be in here
2181 msg
.msg
= $('<div />').text(msg
.msg
).html();
2183 // Make the channels clickable
2184 re
= new RegExp('\\B([' + kiwi
.gateway
.get('channel_prefix') + '][^ ,.\\007]+)', 'g');
2185 msg
.msg
= msg
.msg
.replace(re
, function (match
) {
2186 return '<a class="chan">' + match
+ '</a>';
2190 // Make links clickable
2191 msg
.msg
= msg
.msg
.replace(/((https?\:\/\/|ftp\:\/\/)|(www\.))(\S+)(\w{2,4})(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]*))?/gi, function (url
) {
2194 // Add the http is no protoocol was found
2195 if (url
.match(/^www\./)) {
2196 url
= 'http://' + url
;
2200 if (nice
.length
> 100) {
2201 nice
= nice
.substr(0, 100) + '...';
2204 return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url
+ '">' + nice
+ '</a>';
2208 // Convert IRC formatting into HTML formatting
2209 msg
.msg
= formatIRCMsg(msg
.msg
);
2212 // Add some colours to the nick (Method based on IRSSIs nickcolor.pl)
2213 nick_colour_hex
= (function (nick
) {
2214 var nick_int
= 0, rgb
;
2216 _
.map(nick
.split(''), function (i
) { nick_int
+= i
.charCodeAt(0); });
2217 rgb
= hsl2rgb(nick_int
% 255, 70, 35);
2218 rgb
= rgb
[2] | (rgb
[1] << 8) | (rgb
[0] << 16);
2220 return '#' + rgb
.toString(16);
2223 msg
.nick_style
= 'color:' + nick_colour_hex
+ ';';
2225 // Build up and add the line
2226 line_msg
= '<div class="msg <%= type %>"><div class="time"><%- time %></div><div class="nick" style="<%= nick_style %>"><%- nick %></div><div class="text" style="<%= style %>"><%= msg %> </div></div>';
2227 $this.append(_
.template(line_msg
, msg
));
2229 this.scrollToBottom();
2231 // Make sure our DOM isn't getting too large (Acts as scrollback)
2233 if (this.msg_count
> 250) {
2234 $('.msg:first', this.div
).remove();
2238 chanClick: function (x
) {
2239 kiwi
.gateway
.join($(x
.srcElement
).text());
2242 var $this = this.$el
;
2244 // Hide all other panels and show this one
2245 this.$container
.children().css('display', 'none');
2246 $this.css('display', 'block');
2248 // Show this panels memberlist
2249 var members
= this.model
.get("members");
2251 members
.view
.show();
2252 this.$container
.parent().css('right', '200px');
2254 // Memberlist not found for this panel, hide any active ones
2255 $('#memberlists').children().removeClass('active');
2256 this.$container
.parent().css('right', '0');
2259 this.scrollToBottom();
2261 this.trigger('active', this.model
);
2262 kiwi
.app
.panels
.trigger('active', this.model
);
2266 // Scroll to the bottom of the panel
2267 scrollToBottom: function () {
2268 // TODO: Don't scroll down if we're scrolled up the panel a little
2269 this.$container
[0].scrollTop
= this.$container
[0].scrollHeight
;
2273 kiwi
.view
.Channel
= kiwi
.view
.Panel
.extend({
2274 initialize: function (options
) {
2275 this.initializePanel(options
);
2276 this.model
.bind('change:topic', this.topic
, this);
2279 topic: function (topic
) {
2280 if (typeof topic
!== 'string' || !topic
) {
2281 topic
= this.model
.get("topic");
2284 this.model
.addMsg('', '=== Topic for ' + this.model
.get('name') + ' is: ' + topic
, 'topic');
2286 // If this is the active channel then update the topic bar
2287 if (kiwi
.app
.panels
.active
=== this) {
2288 kiwi
.app
.topicbar
.setCurrentTopic(this.model
.get("topic"));
2293 // Model for this = kiwi.model.PanelList
2294 kiwi
.view
.Tabs
= Backbone
.View
.extend({
2296 "click li": "tabClick",
2297 'click li img': 'partClick'
2300 initialize: function () {
2301 this.model
.on("add", this.panelAdded
, this);
2302 this.model
.on("remove", this.panelRemoved
, this);
2303 this.model
.on("reset", this.render
, this);
2305 this.model
.on('active', this.panelActive
, this);
2307 kiwi
.gateway
.on('change:name', function (gateway
, new_val
) {
2308 $('span', this.model
.server
.tab
).text(new_val
);
2311 render: function () {
2316 // Add the server tab first
2317 this.model
.server
.tab
2318 .data('panel_id', this.model
.server
.cid
)
2319 .appendTo(this.$el
);
2321 // Go through each panel adding its tab
2322 this.model
.forEach(function (panel
) {
2323 // If this is the server panel, ignore as it's already added
2324 if (panel
== that
.model
.server
) return;
2327 .data('panel_id', panel
.cid
)
2328 .appendTo(this.$el
);
2332 panelAdded: function (panel
) {
2333 // Add a tab to the panel
2334 panel
.tab
= $('<li><span>' + panel
.get("name") + '</span></li>');
2335 panel
.tab
.data('panel_id', panel
.cid
)
2336 .appendTo(this.$el
);
2338 panelRemoved: function (panel
) {
2343 panelActive: function (panel
) {
2344 // Remove any existing tabs or part images
2345 $('img', this.$el
).remove();
2346 this.$el
.children().removeClass('active');
2348 panel
.tab
.addClass('active');
2349 panel
.tab
.append('<img src="img/redcross.png" />');
2352 tabClick: function (e
) {
2353 var tab
= $(e
.currentTarget
);
2355 var panel
= this.model
.getByCid(tab
.data('panel_id'));
2357 // A panel wasn't found for this tab... wadda fuck
2364 partClick: function (e
) {
2365 var tab
= $(e
.currentTarget
).parent();
2366 var panel
= this.model
.getByCid(tab
.data('panel_id'));
2368 // Only need to part if it's a channel
2369 if (panel
.isChannel()) {
2370 kiwi
.gateway
.part(panel
.get('name'));
2377 var next
= kiwi
.app
.panels
.active
.tab
.next();
2378 if (!next
.length
) next
= $('li:first', this.$el
);
2383 var prev
= kiwi
.app
.panels
.active
.tab
.prev();
2384 if (!prev
.length
) prev
= $('li:last', this.$el
);
2392 kiwi
.view
.TopicBar
= Backbone
.View
.extend({
2394 'keydown input': 'process'
2397 initialize: function () {
2398 kiwi
.app
.panels
.bind('active', function (active_panel
) {
2399 this.setCurrentTopic(active_panel
.get('topic'));
2403 process: function (ev
) {
2404 var inp
= $(ev
.currentTarget
),
2405 inp_val
= inp
.val();
2407 if (ev
.keyCode
!== 13) return;
2409 if (kiwi
.app
.panels
.active
.isChannel()) {
2410 kiwi
.gateway
.topic(kiwi
.app
.panels
.active
.get('name'), inp_val
);
2414 setCurrentTopic: function (new_topic
) {
2415 new_topic
= new_topic
|| '';
2417 // We only want a plain text version
2418 new_topic
= $('<div>').html(formatIRCMsg(new_topic
));
2419 $('input', this.$el
).val(new_topic
.text());
2425 kiwi
.view
.ControlBox
= Backbone
.View
.extend({
2426 buffer
: [], // Stores previously run commands
2427 buffer_pos
: 0, // The current position in the buffer
2429 // Hold tab autocomplete data
2430 tabcomplete
: {active
: false, data
: [], prefix
: ''},
2433 'keydown input': 'process'
2436 initialize: function () {
2439 kiwi
.gateway
.bind('change:nick', function () {
2440 $('.nick', that
.$el
).text(this.get('nick'));
2444 process: function (ev
) {
2446 inp
= $(ev
.currentTarget
),
2447 inp_val
= inp
.val(),
2450 if (navigator
.appVersion
.indexOf("Mac") !== -1) {
2456 // If not a tab key, reset the tabcomplete data
2457 if (this.tabcomplete
.active
&& ev
.keyCode
!== 9) {
2458 this.tabcomplete
.active
= false;
2459 this.tabcomplete
.data
= [];
2460 this.tabcomplete
.prefix
= '';
2464 case (ev
.keyCode
=== 13): // return
2465 inp_val
= inp_val
.trim();
2468 this.processInput(inp
.val());
2470 this.buffer
.push(inp
.val());
2471 this.buffer_pos
= this.buffer
.length
;
2478 case (ev
.keyCode
=== 38): // up
2479 if (this.buffer_pos
> 0) {
2481 inp
.val(this.buffer
[this.buffer_pos
]);
2485 case (ev
.keyCode
=== 40): // down
2486 if (this.buffer_pos
< this.buffer
.length
) {
2488 inp
.val(this.buffer
[this.buffer_pos
]);
2492 case (ev
.keyCode
=== 37 && meta
): // left
2493 kiwi
.app
.panels
.view
.prev();
2496 case (ev
.keyCode
=== 39 && meta
): // right
2497 kiwi
.app
.panels
.view
.next();
2500 case (ev
.keyCode
=== 9): // tab
2501 this.tabcomplete
.active
= true;
2502 if (_
.isEqual(this.tabcomplete
.data
, [])) {
2503 // Get possible autocompletions
2505 $.each(kiwi
.app
.panels
.active
.get('members').models
, function (i
, member
) {
2506 if (!member
) return;
2507 ac_data
.push(member
.get('nick'));
2509 ac_data
= _
.sortBy(ac_data
, function (nick
) {
2512 this.tabcomplete
.data
= ac_data
;
2515 if (inp_val
[inp
[0].selectionStart
- 1] === ' ') {
2520 var tokens
= inp_val
.substring(0, inp
[0].selectionStart
).split(' '),
2525 nick
= tokens
[tokens
.length
- 1];
2526 if (this.tabcomplete
.prefix
=== '') {
2527 this.tabcomplete
.prefix
= nick
;
2530 this.tabcomplete
.data
= _
.select(this.tabcomplete
.data
, function (n
) {
2531 return (n
.toLowerCase().indexOf(that
.tabcomplete
.prefix
.toLowerCase()) === 0);
2534 if (this.tabcomplete
.data
.length
> 0) {
2535 p1
= inp
[0].selectionStart
- (nick
.length
);
2536 val
= inp_val
.substr(0, p1
);
2537 newnick
= this.tabcomplete
.data
.shift();
2538 this.tabcomplete
.data
.push(newnick
);
2540 val
+= inp_val
.substr(inp
[0].selectionStart
);
2543 if (inp
[0].setSelectionRange
) {
2544 inp
[0].setSelectionRange(p1
+ newnick
.length
, p1
+ newnick
.length
);
2545 } else if (inp
[0].createTextRange
) { // not sure if this bit is actually needed....
2546 range
= inp
[0].createTextRange();
2547 range
.collapse(true);
2548 range
.moveEnd('character', p1
+ newnick
.length
);
2549 range
.moveStart('character', p1
+ newnick
.length
);
2559 processInput: function (command_raw
) {
2561 params
= command_raw
.split(' ');
2563 // Extract the command and parameters
2564 if (params
[0][0] === '/') {
2565 command
= params
[0].substr(1).toLowerCase();
2566 params
= params
.splice(1);
2570 params
.unshift(kiwi
.app
.panels
.active
.get('name'));
2573 // Trigger the command events
2574 this.trigger('command', {command
: command
, params
: params
});
2575 this.trigger('command_' + command
, {command
: command
, params
: params
});
2577 // If we didn't have any listeners for this event, fire a special case
2578 // TODO: This feels dirty. Should this really be done..?
2579 if (!this._callbacks
['command_' + command
]) {
2580 this.trigger('unknown_command', {command
: command
, params
: params
});
2588 kiwi
.view
.StatusMessage
= Backbone
.View
.extend({
2589 /* Timer for hiding the message */
2592 initialize: function () {
2596 text: function (text
, opt
) {
2599 opt
.type
= opt
.type
|| '';
2601 this.$el
.text(text
).attr('class', opt
.type
);
2602 this.$el
.slideDown(kiwi
.app
.view
.doLayout
);
2604 if (opt
.timeout
) this.doTimeout(opt
.timeout
);
2607 html: function (html
, opt
) {
2610 opt
.type
= opt
.type
|| '';
2612 this.$el
.html(text
).attr('class', opt
.type
);
2613 this.$el
.slideDown(kiwi
.app
.view
.doLayout
);
2615 if (opt
.timeout
) this.doTimeout(opt
.timeout
);
2619 this.$el
.slideUp(kiwi
.app
.view
.doLayout
);
2622 doTimeout: function (length
) {
2623 if (this.tmr
) clearTimeout(this.tmr
);
2625 this.tmr
= setTimeout(function () { that
.hide(); }, length
);
2632 kiwi
.view
.Application
= Backbone
.View
.extend({
2633 initialize: function () {
2634 $(window
).resize(this.doLayout
);
2635 $('#toolbar').resize(this.doLayout
);
2636 $('#controlbox').resize(this.doLayout
);
2640 $(document
).keydown(this.setKeyFocus
);
2644 // Globally shift focus to the command input box on a keypress
2645 setKeyFocus: function (ev
) {
2646 // If we're copying text, don't shift focus
2647 if (ev
.ctrlKey
|| ev
.altKey
) {
2651 // If we're typing into an input box somewhere, ignore
2652 if (ev
.target
.tagName
.toLowerCase() === 'input') {
2656 $('#controlbox .inp').focus();
2660 doLayout: function () {
2661 var el_panels
= $('#panels');
2662 var el_memberlists
= $('#memberlists');
2663 var el_toolbar
= $('#toolbar');
2664 var el_controlbox
= $('#controlbox');
2667 top
: el_toolbar
.outerHeight(true),
2668 bottom
: el_controlbox
.outerHeight(true)
2671 el_panels
.css(css_heights
);
2672 el_memberlists
.css(css_heights
);
2676 barsHide: function (instant
) {
2680 $('#toolbar').slideUp();
2681 $('#controlbox').slideUp(function () { that
.doLayout(); });
2683 $('#toolbar').slideUp(0);
2684 $('#controlbox').slideUp(0);
2688 barsShow: function (instant
) {
2692 $('#toolbar').slideDown();
2693 $('#controlbox').slideDown(function () { that
.doLayout(); });
2695 $('#toolbar').slideDown(0);
2696 $('#controlbox').slideDown(0);