Kiwi globals; Built kiwi within closure
[KiwiIRC.git] / client_backbone / kiwi.js
1 (function (global) {
2
3 // Holds anything kiwi client specific (ie. front, gateway, kiwi.plugs..)
4 /**
5 * @namespace
6 */
7 var kiwi = {};
8
9 kiwi.model = {};
10 kiwi.view = {};
11
12
13 /**
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)
17 */
18 kiwi.global = {
19 gateway: undefined,
20 user: undefined,
21 server: undefined,
22 channels: undefined,
23
24 // Entry point to start the kiwi application
25 start: function (opts) {
26 opts = opts || {};
27
28 kiwi.app = new kiwi.model.Application(opts);
29
30 if (opts.kiwi_server) {
31 kiwi.app.kiwi_server = opts.kiwi_server;
32 }
33
34 kiwi.app.start();
35
36 return true;
37 },
38
39 utils: undefined // Re-usable methods
40 };
41
42
43
44 // If within a closure, expose the kiwi globals
45 if (typeof global !== 'undefined') {
46 global.kiwi = kiwi.global;
47 }
48
49
50 kiwi.model.Application = Backbone.Model.extend(new (function () {
51 var that = this;
52
53 // The auto connect details entered into the server select box
54 var auto_connect_details = {};
55
56 /** Instance of kiwi.model.PanelList */
57 this.panels = null;
58
59 /** kiwi.view.Application */
60 this.view;
61
62 /** kiwi.view.StatusMessage */
63 this.message;
64
65 /* Address for the kiwi server */
66 this.kiwi_server = null;
67
68 this.initialize = function () {
69 // Update `that` with this new Model object
70 that = this;
71
72 // Best guess at where the kiwi server is
73 this.detectKiwiServer();
74 };
75
76 this.start = function () {
77 // Only debug if set in the querystring
78 if (!getQueryVariable('debug')) {
79 //manageDebug(false);
80 } else {
81 //manageDebug(true);
82 }
83
84 // Set the gateway up
85 kiwi.gateway = new kiwi.model.Gateway();
86 this.bindGatewayCommands(kiwi.gateway);
87
88 this.initializeClient();
89 this.view.barsHide(true);
90
91 this.panels.server.server_login.bind('server_connect', function (event) {
92 var server_login = this;
93 auto_connect_details = event;
94
95 server_login.networkConnecting();
96
97 $script(that.kiwi_server + '/socket.io/socket.io.js?ts='+(new Date().getTime()), function () {
98 if (!window.io) {
99 kiwiServerNotFound();
100 return;
101 }
102 kiwi.gateway.set('kiwi_server', that.kiwi_server + '/kiwi');
103 kiwi.gateway.set('nick', event.nick);
104
105 kiwi.gateway.connect(event.server, event.port, event.ssl, event.password, function () {});
106 });
107 });
108
109 };
110
111
112 function kiwiServerNotFound (e) {
113 that.panels.server.server_login.showError();
114 }
115
116
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';
121
122 } else {
123 // Assume the kiwi server is on the same server
124 var proto = window.location.protocol === 'https' ?
125 'https' :
126 'http';
127
128 this.kiwi_server = proto + '://' + window.location.host + ':' + (window.location.port || '80');
129 }
130
131 };
132
133
134 this.initializeClient = function () {
135 this.view = new kiwi.view.Application({model: this, el: this.get('container')});
136
137
138 /**
139 * Set the UI components up
140 */
141 this.panels = new kiwi.model.PanelList();
142
143 this.controlbox = new kiwi.view.ControlBox({el: $('#controlbox')[0]});
144 this.bindControllboxCommands(this.controlbox);
145
146 this.topicbar = new kiwi.view.TopicBar({el: $('#topic')[0]});
147
148 this.message = new kiwi.view.StatusMessage({el: $('#status_message')[0]});
149
150
151 this.panels.server.view.show();
152
153 // Rejigg the UI sizes
154 this.view.doLayout();
155
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',
160 port: 6667,
161 ssl: false,
162 channel: window.location.hash || '#test'
163 });
164 };
165
166
167
168 this.bindGatewayCommands = function (gw) {
169 gw.on('onmotd', function (event) {
170 that.panels.server.addMsg(event.server, event.msg, 'motd');
171 });
172
173
174 gw.on('onconnect', function (event) {
175 that.view.barsShow();
176
177 if (auto_connect_details.channel) {
178 kiwi.gateway.join(auto_connect_details.channel);
179 }
180 });
181
182
183 (function () {
184 var gw_stat = 0;
185
186 gw.on('disconnect', function (event) {
187 that.message.text('You have been disconnected. Attempting to reconnect..');
188 gw_stat = 1;
189 });
190 gw.on('reconnecting', function (event) {
191 that.message.text('You have been disconnected. Attempting to reconnect again in ' + (event.delay/1000) + ' seconds..');
192 });
193 gw.on('connect', function (event) {
194 if (gw_stat !== 1) return;
195
196 that.message.text('It\'s OK, you\'re connected again :)', {timeout: 5000});
197 gw_stat = 0;
198 });
199 })();
200
201
202 gw.on('onjoin', function (event) {
203 var c, members, user;
204 c = that.panels.getByName(event.channel);
205 if (!c) {
206 c = new kiwi.model.Channel({name: event.channel});
207 that.panels.add(c);
208 }
209
210 members = c.get('members');
211 if (!members) return;
212
213 user = new kiwi.model.Member({nick: event.nick, ident: event.ident, hostname: event.hostname});
214 members.add(user);
215 // TODO: highlight the new channel in some way
216 });
217
218
219 gw.on('onpart', function (event) {
220 var channel, members, user,
221 part_options = {};
222
223 part_options.type = 'part';
224 part_options.message = event.message || '';
225
226 channel = that.panels.getByName(event.channel);
227 if (!channel) return;
228
229 // If this is us, close the panel
230 if (event.nick === kiwi.gateway.get('nick')) {
231 channel.close();
232 return;
233 }
234
235 members = channel.get('members');
236 if (!members) return;
237
238 user = members.getByNick(event.nick);
239 if (!user) return;
240
241 members.remove(user, part_options);
242 });
243
244
245 gw.on('onquit', function (event) {
246 var member, members,
247 quit_options = {};
248
249 quit_options.type = 'quit';
250 quit_options.message = event.message || '';
251
252 $.each(that.panels.models, function (index, panel) {
253 if (!panel.isChannel()) return;
254
255 member = panel.get('members').getByNick(event.nick);
256 if (member) {
257 panel.get('members').remove(member, quit_options);
258 }
259 });
260 });
261
262
263 gw.on('onkick', function (event) {
264 var channel, members, user,
265 part_options = {};
266
267 part_options.type = 'kick';
268 part_options.by = event.nick;
269 part_options.message = event.message || '';
270
271 channel = that.panels.getByName(event.channel);
272 if (!channel) return;
273
274 members = channel.get('members');
275 if (!members) return;
276
277 user = members.getByNick(event.kicked);
278 if (!user) return;
279
280 members.remove(user, part_options);
281
282 if (event.kicked === kiwi.gateway.get('nick')) {
283 members.reset([]);
284 }
285
286 });
287
288
289 gw.on('onmsg', function (event) {
290 var panel,
291 is_pm = (event.channel == kiwi.gateway.get('nick'));
292
293 if (is_pm) {
294 // If a panel isn't found for this PM, create one
295 panel = that.panels.getByName(event.nick);
296 if (!panel) {
297 panel = new kiwi.model.Channel({name: event.nick});
298 that.panels.add(panel);
299 }
300
301 } else {
302 // If a panel isn't found for this channel, reroute to the
303 // server panel
304 panel = that.panels.getByName(event.channel);
305 if (!panel) {
306 panel = that.panels.server;
307 }
308 }
309
310 panel.addMsg(event.nick, event.msg);
311 });
312
313
314 gw.on('onnotice', function (event) {
315 var panel;
316
317 // Find a panel for the destination(channel) or who its from
318 panel = that.panels.getByName(event.target) || that.panels.getByName(event.nick);
319 if (!panel) {
320 panel = that.panels.server;
321 }
322
323 panel.addMsg('[' + (event.nick||'') + ']', event.msg);
324 });
325
326
327 gw.on('onaction', function (event) {
328 var panel,
329 is_pm = (event.channel == kiwi.gateway.get('nick'));
330
331 if (is_pm) {
332 // If a panel isn't found for this PM, create one
333 panel = that.panels.getByName(event.nick);
334 if (!panel) {
335 panel = new kiwi.model.Channel({name: event.nick});
336 that.panels.add(panel);
337 }
338
339 } else {
340 // If a panel isn't found for this channel, reroute to the
341 // server panel
342 panel = that.panels.getByName(event.channel);
343 if (!panel) {
344 panel = that.panels.server;
345 }
346 }
347
348 panel.addMsg('', '* ' + event.nick + ' ' + event.msg, 'action');
349 });
350
351
352 gw.on('ontopic', function (event) {
353 var c;
354 c = that.panels.getByName(event.channel);
355 if (!c) return;
356
357 // Set the channels topic
358 c.set('topic', event.topic);
359
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);
363 }
364 });
365
366
367 gw.on('ontopicsetby', function (event) {
368 var c, when;
369 c = that.panels.getByName(event.channel);
370 if (!c) return;
371
372 when = new Date(event.when * 1000).toLocaleString();
373 c.addMsg('', 'Topic set by ' + event.nick + ' at ' + when, 'topic');
374 });
375
376
377 gw.on('onuserlist', function (event) {
378 var channel;
379 channel = that.panels.getByName(event.channel);
380
381 // If we didn't find a channel for this, may aswell leave
382 if (!channel) return;
383
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);
388 });
389 });
390
391
392 gw.on('onuserlist_end', function (event) {
393 var channel;
394 channel = that.panels.getByName(event.channel);
395
396 // If we didn't find a channel for this, may aswell leave
397 if (!channel) return;
398
399 // Update the members list with the new list
400 channel.get('members').reset(channel.temp_userlist || []);
401
402 // Clear the temporary userlist
403 delete channel.temp_userlist;
404 });
405
406
407 gw.on('onmode', function (event) {
408 var channel, members, member;
409
410 if (!event.channel) return;
411 channel = that.panels.getByName(event.channel);
412 if (!channel) return;
413
414 members = channel.get('members');
415 if (!members) return;
416
417 member = members.getByNick(event.effected_nick);
418 if (!member) return;
419
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));
424 }
425 });
426
427
428 gw.on('onnick', function (event) {
429 var member;
430
431 $.each(that.panels.models, function (index, panel) {
432 if (!panel.isChannel()) return;
433
434 member = panel.get('members').getByNick(event.nick);
435 if (member) {
436 member.set('nick', event.newnick);
437 panel.addMsg('', '== ' + event.nick + ' is now known as ' + event.newnick, 'action nick');
438 }
439 });
440 });
441 };
442
443
444
445 /**
446 * Bind to certain commands that may be typed into the control box
447 */
448 this.bindControllboxCommands = function (controlbox) {
449 controlbox.on('unknown_command', this.unknownCommand);
450
451 controlbox.on('command', this.allCommands);
452 controlbox.on('command_msg', this.msgCommand);
453
454 controlbox.on('command_action', this.actionCommand);
455 controlbox.on('command_me', this.actionCommand);
456
457 controlbox.on('command_join', this.joinCommand);
458 controlbox.on('command_j', this.joinCommand);
459
460 controlbox.on('command_part', this.partCommand);
461 controlbox.on('command_p', this.partCommand);
462
463 controlbox.on('command_nick', function (ev) {
464 kiwi.gateway.changeNick(ev.params[0]);
465 });
466
467 controlbox.on('command_query', this.queryCommand);
468 controlbox.on('command_q', this.queryCommand);
469
470 controlbox.on('command_topic', this.topicCommand);
471
472 controlbox.on('command_notice', this.noticeCommand);
473
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);
478 });
479 });
480 };
481
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);
487 };
488
489 this.allCommands = function (ev) {
490 console.log('allCommands', ev);
491 };
492
493 this.joinCommand = function (ev) {
494 var channel, channel_names;
495
496 channel_names = ev.params.join(' ').split(',');
497
498 $.each(channel_names, function (index, channel_name) {
499 // Trim any whitespace off the name
500 channel_name = channel_name.trim();
501
502 // Check if we have the panel already. If not, create it
503 channel = that.panels.getByName(channel_name);
504 if (!channel) {
505 channel = new kiwi.model.Channel({name: channel_name});
506 kiwi.app.panels.add(channel);
507 }
508
509 kiwi.gateway.join(channel_name);
510 });
511
512 if (channel) channel.view.show();
513
514 };
515
516 this.queryCommand = function (ev) {
517 var destination, panel;
518
519 destination = ev.params[0];
520
521 // Check if we have the panel already. If not, create it
522 panel = that.panels.getByName(destination);
523 if (!panel) {
524 panel = new kiwi.model.Channel({name: destination});
525 kiwi.app.panels.add(panel);
526 }
527
528 if (panel) panel.view.show();
529
530 };
531
532 this.msgCommand = function (ev) {
533 var destination = ev.params[0],
534 panel = that.panels.getByName(destination) || that.panels.server;
535
536 ev.params.shift();
537
538 panel.addMsg(kiwi.gateway.get('nick'), ev.params.join(' '));
539 kiwi.gateway.privmsg(destination, ev.params.join(' '));
540 };
541
542 this.actionCommand = function (ev) {
543 if (kiwi.app.panels.active === kiwi.app.panels.server) {
544 return;
545 }
546
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(' '));
550 };
551
552 this.partCommand = function (ev) {
553 if (ev.params.length === 0) {
554 kiwi.gateway.part(kiwi.app.panels.active.get('name'));
555 } else {
556 _.each(ev.params, function (channel) {
557 kiwi.gateway.part(channel);
558 });
559 }
560 // TODO: More responsive = close tab now, more accurate = leave until part event
561 //kiwi.app.panels.remove(kiwi.app.panels.active);
562 };
563
564 this.topicCommand = function (ev) {
565 var channel_name;
566
567 if (ev.params.length === 0) return;
568
569 if (that.isChannelName(ev.params[0])) {
570 channel_name = ev.params[0];
571 ev.params.shift();
572 } else {
573 channel_name = kiwi.app.panels.active.get('name');
574 }
575
576 kiwi.gateway.topic(channel_name, ev.params.join(' '));
577 };
578
579 this.noticeCommand = function (ev) {
580 var destination;
581
582 // Make sure we have a destination and some sort of message
583 if (ev.params.length <= 1) return;
584
585 destination = ev.params[0];
586 ev.params.shift();
587
588 kiwi.gateway.notice(destination, ev.params.join(' '));
589 };
590
591
592
593
594
595 this.isChannelName = function (channel_name) {
596 var channel_prefix = kiwi.gateway.get('channel_prefix');
597
598 if (!channel_name || !channel_name.length) return false;
599 return (channel_prefix.indexOf(channel_name[0]) > -1);
600 };
601
602 })());
603
604
605 kiwi.model.Gateway = Backbone.Model.extend(new (function () {
606 var that = this;
607
608 this.defaults = {
609 /**
610 * The name of the network
611 * @type String
612 */
613 name: 'Server',
614
615 /**
616 * The address (URL) of the network
617 * @type String
618 */
619 address: '',
620
621 /**
622 * The current nickname
623 * @type String
624 */
625 nick: '',
626
627 /**
628 * The channel prefix for this network
629 * @type String
630 */
631 channel_prefix: '#',
632
633 /**
634 * The user prefixes for channel owner/admin/op/voice etc. on this network
635 * @type Array
636 */
637 user_prefixes: ['~', '&', '@', '+'],
638
639 /**
640 * The URL to the Kiwi server
641 * @type String
642 */
643 //kiwi_server: '//kiwi'
644 kiwi_server: 'http://localhost:7778/kiwi'
645 };
646
647
648 this.initialize = function () {
649 // Update `that` with this new Model object
650 that = this;
651
652 // For ease of access. The socket.io object
653 this.socket = this.get('socket');
654
655 // Redundant perhaps? Legacy
656 this.session_id = '';
657
658 network = this;
659 };
660
661
662 /**
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
669 */
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
676 });
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});
684 });
685
686 this.socket.on('error', function (e) {
687 this.emit("connect_fail", {reason: e});
688 console.log("kiwi.gateway.socket.on('error')", {reason: e});
689 });
690
691 this.socket.on('connecting', function (transport_type) {
692 console.log("kiwi.gateway.socket.on('connecting')");
693 this.emit("connecting");
694 that.trigger("connecting");
695 });
696
697 this.socket.on('connect', function () {
698 this.emit('irc connect', that.get('nick'), host, port, ssl, password, callback);
699 that.trigger('connect', {});
700 });
701
702 this.socket.on('too_many_connections', function () {
703 this.emit("connect_fail", {reason: 'too_many_connections'});
704 });
705
706 this.socket.on('message', this.parse);
707
708 this.socket.on('disconnect', function () {
709 that.trigger("disconnect", {});
710 console.log("kiwi.gateway.socket.on('disconnect')");
711 });
712
713 this.socket.on('close', function () {
714 console.log("kiwi.gateway.socket.on('close')");
715 });
716
717 this.socket.on('reconnecting', function (reconnectionDelay, reconnectionAttempts) {
718 console.log("kiwi.gateway.socket.on('reconnecting')");
719 that.trigger("reconnecting", {delay: reconnectionDelay, attempts: reconnectionAttempts});
720 });
721
722 this.socket.on('reconnect_failed', function () {
723 console.log("kiwi.gateway.socket.on('reconnect_failed')");
724 });
725 };
726
727
728 /*
729 Events:
730 msg
731 action
732 server_connect
733 options
734 motd
735 notice
736 userlist
737 nick
738 join
739 topic
740 part
741 kick
742 quit
743 whois
744 syncchannel_redirect
745 debug
746 */
747 /**
748 * Parses the response from the server
749 */
750 this.parse = function (item) {
751 //console.log('gateway event', item);
752 if (item.event !== undefined) {
753 that.trigger('on' + item.event, item);
754
755 switch (item.event) {
756 case 'options':
757 $.each(item.options, function (name, value) {
758 switch (name) {
759 case 'CHANTYPES':
760 // TODO: Check this. Why is it only getting the first char?
761 that.set('channel_prefix', value.charAt(0));
762 break;
763 case 'NETWORK':
764 that.set('name', value);
765 break;
766 case 'PREFIX':
767 that.set('user_prefixes', value);
768 break;
769 }
770 });
771 break;
772
773 case 'connect':
774 that.set('nick', item.nick);
775 break;
776
777 case 'nick':
778 if (item.nick === that.get('nick')) {
779 that.set('nick', item.newnick);
780 }
781 break;
782 /*
783 case 'sync':
784 if (kiwi.gateway.onSync && kiwi.gateway.syncing) {
785 kiwi.gateway.syncing = false;
786 kiwi.gateway.onSync(item);
787 }
788 break;
789 */
790
791 case 'kiwi':
792 this.emit('kiwi.' + item.namespace, item.data);
793 break;
794 }
795 }
796 };
797
798 /**
799 * Sends data to the server
800 * @private
801 * @param {Object} data The data to send
802 * @param {Function} callback A callback function
803 */
804 this.sendData = function (data, callback) {
805 this.socket.emit('message', {sid: this.session_id, data: JSON.stringify(data)}, callback);
806 };
807
808 /**
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
813 */
814 this.privmsg = function (target, msg, callback) {
815 var data = {
816 method: 'privmsg',
817 args: {
818 target: target,
819 msg: msg
820 }
821 };
822
823 this.sendData(data, callback);
824 };
825
826 /**
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
831 */
832 this.notice = function (target, msg, callback) {
833 var data = {
834 method: 'notice',
835 args: {
836 target: target,
837 msg: msg
838 }
839 };
840
841 this.sendData(data, callback);
842 };
843
844 /**
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
851 */
852 this.ctcp = function (request, type, target, params, callback) {
853 var data = {
854 method: 'ctcp',
855 args: {
856 request: request,
857 type: type,
858 target: target,
859 params: params
860 }
861 };
862
863 this.sendData(data, callback);
864 };
865
866 /**
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
870 */
871 this.action = function (target, msg, callback) {
872 this.ctcp(true, 'ACTION', target, msg, callback);
873 };
874
875 /**
876 * Joins a channel
877 * @param {String} channel The channel to join
878 * @param {String} key The key to the channel
879 * @param {Function} callback A callback function
880 */
881 this.join = function (channel, key, callback) {
882 var data = {
883 method: 'join',
884 args: {
885 channel: channel,
886 key: key
887 }
888 };
889
890 this.sendData(data, callback);
891 };
892
893 /**
894 * Leaves a channel
895 * @param {String} channel The channel to part
896 * @param {Function} callback A callback function
897 */
898 this.part = function (channel, callback) {
899 var data = {
900 method: 'part',
901 args: {
902 channel: channel
903 }
904 };
905
906 this.sendData(data, callback);
907 };
908
909 /**
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
914 */
915 this.topic = function (channel, new_topic, callback) {
916 var data = {
917 method: 'topic',
918 args: {
919 channel: channel,
920 topic: new_topic
921 }
922 };
923
924 this.sendData(data, callback);
925 };
926
927 /**
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
933 */
934 this.kick = function (channel, nick, reason, callback) {
935 var data = {
936 method: 'kick',
937 args: {
938 channel: channel,
939 nick: nick,
940 reason: reason
941 }
942 };
943
944 this.sendData(data, callback);
945 };
946
947 /**
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
951 */
952 this.quit = function (msg, callback) {
953 msg = msg || "";
954 var data = {
955 method: 'quit',
956 args: {
957 message: msg
958 }
959 };
960
961 this.sendData(data, callback);
962 };
963
964 /**
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
968 */
969 this.raw = function (data, callback) {
970 data = {
971 method: 'raw',
972 args: {
973 data: data
974 }
975 };
976
977 this.sendData(data, callback);
978 };
979
980 /**
981 * Changes our nickname
982 * @param {String} new_nick Our new nickname
983 * @param {Function} callback A callback function
984 */
985 this.changeNick = function (new_nick, callback) {
986 var data = {
987 method: 'nick',
988 args: {
989 nick: new_nick
990 }
991 };
992
993 this.sendData(data, callback);
994 };
995
996 /**
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
1001 */
1002 this.kiwi = function (target, data, callback) {
1003 data = {
1004 method: 'kiwi',
1005 args: {
1006 target: target,
1007 data: data
1008 }
1009 };
1010
1011 this.sendData(data, callback);
1012 };
1013 })());
1014
1015
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');
1021
1022 for (i = 0; i < user_prefixes.length; i++) {
1023 if (user_prefixes[i].mode === a) {
1024 a_idx = i;
1025 }
1026 }
1027 for (i = 0; i < user_prefixes.length; i++) {
1028 if (user_prefixes[i].mode === b) {
1029 b_idx = i;
1030 }
1031 }
1032 if (a_idx < b_idx) {
1033 return -1;
1034 } else if (a_idx > b_idx) {
1035 return 1;
1036 } else {
1037 return 0;
1038 }
1039 });
1040 },
1041 initialize: function (attributes) {
1042 var nick, modes, prefix;
1043 nick = this.stripPrefix(this.get("nick"));
1044
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});
1049 },
1050 addMode: function (mode) {
1051 var modes_to_add = mode.split(''),
1052 modes, prefix;
1053
1054 modes = this.get("modes");
1055 $.each(modes_to_add, function (index, item) {
1056 modes.push(item);
1057 });
1058
1059 modes = this.sortModes(modes);
1060 this.set({"prefix": this.getPrefix(modes), "modes": modes});
1061 },
1062 removeMode: function (mode) {
1063 var modes_to_remove = mode.split(''),
1064 modes, prefix;
1065
1066 modes = this.get("modes");
1067 modes = _.reject(modes, function (m) {
1068 return (_.indexOf(modes_to_remove, m) !== -1);
1069 });
1070
1071 this.set({"prefix": this.getPrefix(modes), "modes": modes});
1072 },
1073 getPrefix: function (modes) {
1074 var prefix = '';
1075 var user_prefixes = kiwi.gateway.get('user_prefixes');
1076
1077 if (typeof modes[0] !== 'undefined') {
1078 prefix = _.detect(user_prefixes, function (prefix) {
1079 return prefix.mode === modes[0];
1080 });
1081 prefix = (prefix) ? prefix.symbol : '';
1082 }
1083 return prefix;
1084 },
1085 stripPrefix: function (nick) {
1086 var tmp = nick, i, j, k;
1087 var user_prefixes = kiwi.gateway.get('user_prefixes');
1088 i = 0;
1089
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) {
1093 i++;
1094 break;
1095 }
1096 }
1097 }
1098
1099 return tmp.substr(i);
1100 },
1101 displayNick: function (full) {
1102 var display = this.get('nick');
1103
1104 if (full) {
1105 if (this.get("ident")) {
1106 display += ' [' + this.get("ident") + '@' + this.get("hostname") + ']';
1107 }
1108 }
1109
1110 return display;
1111 }
1112 });
1113
1114
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) {
1126 return -1;
1127 }
1128 a_idx = b_idx = -1;
1129 // Compare the first (highest) mode
1130 for (i = 0; i < user_prefixes.length; i++) {
1131 if (user_prefixes[i].mode === a_modes[0]) {
1132 a_idx = i;
1133 }
1134 }
1135 for (i = 0; i < user_prefixes.length; i++) {
1136 if (user_prefixes[i].mode === b_modes[0]) {
1137 b_idx = i;
1138 }
1139 }
1140 if (a_idx < b_idx) {
1141 return -1;
1142 } else if (a_idx > b_idx) {
1143 return 1;
1144 }
1145 // If we get to here both a and b have the same highest mode so have to resort to lexicographical sorting
1146
1147 } else if (b_modes.length > 0) {
1148 // b has modes but a doesn't so b should appear first
1149 return 1;
1150 }
1151 a_nick = a.get("nick").toLocaleUpperCase();
1152 b_nick = b.get("nick").toLocaleUpperCase();
1153 // Lexicographical sorting
1154 if (a_nick < b_nick) {
1155 return -1;
1156 } else if (a_nick > b_nick) {
1157 return 1;
1158 } else {
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!');
1161 return 0;
1162 }
1163 },
1164 initialize: function (options) {
1165 this.view = new kiwi.view.MemberList({"model": this});
1166 },
1167 getByNick: function (nick) {
1168 if (typeof nick !== 'string') return;
1169 return this.find(function (m) {
1170 return nick.toLowerCase() === m.get('nick').toLowerCase();
1171 });
1172 }
1173 });
1174
1175
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});
1180 this.set({
1181 "scrollback": [],
1182 "name": name
1183 }, {"silent": true});
1184 },
1185
1186 addMsg: function (nick, msg, type, opts) {
1187 var message_obj, bs, d;
1188
1189 opts = opts || {};
1190
1191 // Time defaults to now
1192 if (!opts || typeof opts.time === 'undefined') {
1193 d = new Date();
1194 opts.time = d.getHours().toString().lpad(2, "0") + ":" + d.getMinutes().toString().lpad(2, "0") + ":" + d.getSeconds().toString().lpad(2, "0");
1195 }
1196
1197 // CSS style defaults to empty string
1198 if (!opts || typeof opts.style === 'undefined') {
1199 opts.style = '';
1200 }
1201
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);
1205 if (!message_obj) {
1206 return;
1207 }
1208
1209 // The CSS class (action, topic, notice, etc)
1210 if (typeof message_obj.type !== "string") {
1211 message_obj.type = '';
1212 }
1213
1214 // Make sure we don't have NaN or something
1215 if (typeof message_obj.msg !== "string") {
1216 message_obj.msg = '';
1217 }
1218
1219 // Update the scrollback
1220 bs = this.get("scrollback");
1221 bs.push(message_obj);
1222
1223 // Keep the scrolback limited
1224 if (bs.length > 250) {
1225 bs.splice(250);
1226 }
1227 this.set({"scrollback": bs}, {silent: true});
1228
1229 this.trigger("msg", message_obj);
1230 },
1231
1232 close: function () {
1233 this.view.remove();
1234 delete this.view;
1235
1236 var members = this.get('members');
1237 if (members) {
1238 members.reset([]);
1239 this.unset('members');
1240 }
1241
1242 this.destroy();
1243
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();
1247 }
1248 },
1249
1250 isChannel: function () {
1251 var channel_prefix = kiwi.gateway.get('channel_prefix'),
1252 this_name = this.get('name');
1253
1254 if (!this_name) return false;
1255 return (channel_prefix.indexOf(this_name[0]) > -1);
1256 }
1257 });
1258
1259
1260 kiwi.model.PanelList = Backbone.Collection.extend({
1261 model: kiwi.model.Panel,
1262
1263 // Holds the active panel
1264 active: null,
1265
1266 comparator: function (chan) {
1267 return chan.get("name");
1268 },
1269 initialize: function () {
1270 this.view = new kiwi.view.Tabs({"el": $('#toolbar .panellist')[0], "model": this});
1271
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'));
1275
1276 // Keep a tab on the active panel
1277 this.bind('active', function (active_panel) {
1278 this.active = active_panel;
1279 }, this);
1280
1281 },
1282 getByName: function (name) {
1283 if (typeof name !== 'string') return;
1284 return this.find(function (c) {
1285 return name.toLowerCase() === c.get('name').toLowerCase();
1286 });
1287 }
1288 });
1289
1290
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") || "",
1296 members;
1297
1298 this.view = new kiwi.view.Channel({"model": this, "name": name});
1299 this.set({
1300 "members": new kiwi.model.MemberList(),
1301 "name": name,
1302 "scrollback": [],
1303 "topic": ""
1304 }, {"silent": true});
1305
1306 members = this.get("members");
1307 members.bind("add", function (member) {
1308 this.addMsg(' ', '--> ' + member.displayNick(true) + ' has joined', 'action join');
1309 }, this);
1310
1311 members.bind("remove", function (member, members, options) {
1312 var msg = (options.message) ? '(' + options.message + ')' : '';
1313
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');
1318 } else {
1319 this.addMsg(' ', '<-- ' + member.displayNick(true) + ' has left ' + msg, 'action part');
1320 }
1321 }, this);
1322 }
1323 });
1324
1325
1326 kiwi.model.Server = kiwi.model.Panel.extend({
1327 server_login: null,
1328
1329 initialize: function (attributes) {
1330 var name = "Server";
1331 this.view = new kiwi.view.Panel({"model": this, "name": name});
1332 this.set({
1333 "scrollback": [],
1334 "name": name
1335 }, {"silent": true});
1336
1337 //this.addMsg(' ', '--> Kiwi IRC: Such an awesome IRC client', '', {style: 'color:#009900;'});
1338
1339 this.server_login = new kiwi.view.ServerSelect();
1340
1341 this.view.$el.append(this.server_login.$el);
1342 this.server_login.show();
1343 }
1344 });
1345
1346
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 */
1349
1350
1351
1352 /**
1353 * Suppresses console.log
1354 * @param {Boolean} debug Whether to re-enable console.log or not
1355 */
1356 function manageDebug(debug) {
1357 var log, consoleBackUp;
1358 if (window.console) {
1359 consoleBackUp = window.console.log;
1360 window.console.log = function () {
1361 if (debug) {
1362 consoleBackUp.apply(console, arguments);
1363 }
1364 };
1365 } else {
1366 log = window.opera ? window.opera.postError : alert;
1367 window.console = {};
1368 window.console.log = function (str) {
1369 if (debug) {
1370 log(str);
1371 }
1372 };
1373 }
1374 }
1375
1376 /**
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
1380 */
1381 function randomString(string_length) {
1382 var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz",
1383 randomstring = '',
1384 i,
1385 rnum;
1386 for (i = 0; i < string_length; i++) {
1387 rnum = Math.floor(Math.random() * chars.length);
1388 randomstring += chars.substring(rnum, rnum + 1);
1389 }
1390 return randomstring;
1391 }
1392
1393 /**
1394 * String.trim shim
1395 */
1396 if (typeof String.prototype.trim === 'undefined') {
1397 String.prototype.trim = function () {
1398 return this.replace(/^\s+|\s+$/g, "");
1399 };
1400 }
1401
1402 /**
1403 * String.lpad shim
1404 * @param {Number} length The length of padding
1405 * @param {String} characher The character to pad with
1406 * @returns {String} The padded string
1407 */
1408 if (typeof String.prototype.lpad === 'undefined') {
1409 String.prototype.lpad = function (length, character) {
1410 var padding = "",
1411 i;
1412 for (i = 0; i < length; i++) {
1413 padding += character;
1414 }
1415 return (padding + this).slice(-length);
1416 };
1417 }
1418
1419
1420 /**
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
1424 */
1425 function secondsToTime(secs) {
1426 var hours, minutes, seconds, divisor_for_minutes, divisor_for_seconds, obj;
1427 hours = Math.floor(secs / (60 * 60));
1428
1429 divisor_for_minutes = secs % (60 * 60);
1430 minutes = Math.floor(divisor_for_minutes / 60);
1431
1432 divisor_for_seconds = divisor_for_minutes % 60;
1433 seconds = Math.ceil(divisor_for_seconds);
1434
1435 obj = {
1436 "h": hours,
1437 "m": minutes,
1438 "s": seconds
1439 };
1440 return obj;
1441 }
1442
1443
1444
1445
1446 /**
1447 * Convert HSL to RGB formatted colour
1448 */
1449 function hsl2rgb(h, s, l) {
1450 var m1, m2, hue;
1451 var r, g, b
1452 s /=100;
1453 l /= 100;
1454 if (s == 0)
1455 r = g = b = (l * 255);
1456 else {
1457 function HueToRgb(m1, m2, hue) {
1458 var v;
1459 if (hue < 0)
1460 hue += 1;
1461 else if (hue > 1)
1462 hue -= 1;
1463
1464 if (6 * hue < 1)
1465 v = m1 + (m2 - m1) * hue * 6;
1466 else if (2 * hue < 1)
1467 v = m2;
1468 else if (3 * hue < 2)
1469 v = m1 + (m2 - m1) * (2/3 - hue) * 6;
1470 else
1471 v = m1;
1472
1473 return 255 * v;
1474 }
1475 if (l <= 0.5)
1476 m2 = l * (s + 1);
1477 else
1478 m2 = l + s - l * s;
1479 m1 = l * 2 - m2;
1480 hue = h / 360;
1481 r = HueToRgb(m1, m2, hue + 1/3);
1482 g = HueToRgb(m1, m2, hue);
1483 b = HueToRgb(m1, m2, hue - 1/3);
1484 }
1485 return [r,g,b];
1486 }
1487
1488
1489
1490
1491
1492 /**
1493 * Formats a message. Adds bold, underline and colouring
1494 * @param {String} msg The message to format
1495 * @returns {String} The HTML formatted message
1496 */
1497 function formatIRCMsg (msg) {
1498 var re, next;
1499
1500 if ((!msg) || (typeof msg !== 'string')) {
1501 return '';
1502 }
1503
1504 // bold
1505 if (msg.indexOf(String.fromCharCode(2)) !== -1) {
1506 next = '<b>';
1507 while (msg.indexOf(String.fromCharCode(2)) !== -1) {
1508 msg = msg.replace(String.fromCharCode(2), next);
1509 next = (next === '<b>') ? '</b>' : '<b>';
1510 }
1511 if (next === '</b>') {
1512 msg = msg + '</b>';
1513 }
1514 }
1515
1516 // underline
1517 if (msg.indexOf(String.fromCharCode(31)) !== -1) {
1518 next = '<u>';
1519 while (msg.indexOf(String.fromCharCode(31)) !== -1) {
1520 msg = msg.replace(String.fromCharCode(31), next);
1521 next = (next === '<u>') ? '</u>' : '<u>';
1522 }
1523 if (next === '</u>') {
1524 msg = msg + '</u>';
1525 }
1526 }
1527
1528 // colour
1529 /**
1530 * @inner
1531 */
1532 msg = (function (msg) {
1533 var replace, colourMatch, col, i, match, to, endCol, fg, bg, str;
1534 replace = '';
1535 /**
1536 * @inner
1537 */
1538 colourMatch = function (str) {
1539 var re = /^\x03([0-9][0-5]?)(,([0-9][0-5]?))?/;
1540 return re.exec(str);
1541 };
1542 /**
1543 * @inner
1544 */
1545 col = function (num) {
1546 switch (parseInt(num, 10)) {
1547 case 0:
1548 return '#FFFFFF';
1549 case 1:
1550 return '#000000';
1551 case 2:
1552 return '#000080';
1553 case 3:
1554 return '#008000';
1555 case 4:
1556 return '#FF0000';
1557 case 5:
1558 return '#800040';
1559 case 6:
1560 return '#800080';
1561 case 7:
1562 return '#FF8040';
1563 case 8:
1564 return '#FFFF00';
1565 case 9:
1566 return '#80FF00';
1567 case 10:
1568 return '#008080';
1569 case 11:
1570 return '#00FFFF';
1571 case 12:
1572 return '#0000FF';
1573 case 13:
1574 return '#FF55FF';
1575 case 14:
1576 return '#808080';
1577 case 15:
1578 return '#C0C0C0';
1579 default:
1580 return null;
1581 }
1582 };
1583 if (msg.indexOf('\x03') !== -1) {
1584 i = msg.indexOf('\x03');
1585 replace = msg.substr(0, i);
1586 while (i < msg.length) {
1587 /**
1588 * @inner
1589 */
1590 match = colourMatch(msg.substr(i, 6));
1591 if (match) {
1592 //console.log(match);
1593 // Next colour code
1594 to = msg.indexOf('\x03', i + 1);
1595 endCol = msg.indexOf(String.fromCharCode(15), i + 1);
1596 if (endCol !== -1) {
1597 if (to === -1) {
1598 to = endCol;
1599 } else {
1600 to = ((to < endCol) ? to : endCol);
1601 }
1602 }
1603 if (to === -1) {
1604 to = msg.length;
1605 }
1606 //console.log(i, to);
1607 fg = col(match[1]);
1608 bg = col(match[3]);
1609 str = msg.substring(i + 1 + match[1].length + ((bg !== null) ? match[2].length + 1 : 0), to);
1610 //console.log(str);
1611 replace += '<span style="' + ((fg !== null) ? 'color: ' + fg + '; ' : '') + ((bg !== null) ? 'background-color: ' + bg + ';' : '') + '">' + str + '</span>';
1612 i = to;
1613 } else {
1614 if ((msg[i] !== '\x03') && (msg[i] !== String.fromCharCode(15))) {
1615 replace += msg[i];
1616 }
1617 i++;
1618 }
1619 }
1620 return replace;
1621 }
1622 return msg;
1623 }(msg));
1624
1625 return msg;
1626 }
1627
1628
1629
1630
1631
1632
1633
1634
1635 /*
1636 PLUGINS
1637 Each function in each object is looped through and ran. The resulting text
1638 is expected to be returned.
1639 */
1640 var plugins = [
1641 {
1642 name: "images",
1643 onaddmsg: function (event, opts) {
1644 if (!event.msg) {
1645 return event;
1646 }
1647
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;
1651
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>';
1654 });
1655
1656 return event;
1657 }
1658 },
1659
1660 {
1661 name: "html_safe",
1662 onaddmsg: function (event, opts) {
1663 event.msg = $('<div/>').text(event.msg).html();
1664 event.nick = $('<div/>').text(event.nick).html();
1665
1666 return event;
1667 }
1668 },
1669
1670 {
1671 name: "activity",
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();
1675 //}
1676
1677 return event;
1678 }
1679 },
1680
1681 {
1682 name: "highlight",
1683 onaddmsg: function (event, opts) {
1684 //var tab = Tabviews.getTab(event.tabview.toLowerCase());
1685
1686 // If we have a highlight...
1687 //if (event.msg.toLowerCase().indexOf(kiwi.gateway.nick.toLowerCase()) > -1) {
1688 // if (Tabview.getCurrentTab() !== tab) {
1689 // tab.highlight();
1690 // }
1691 // if (kiwi.front.isChannel(tab.name)) {
1692 // event.msg = '<span style="color:red;">' + event.msg + '</span>';
1693 // }
1694 //}
1695
1696 // If it's a PM, highlight
1697 //if (!kiwi.front.isChannel(tab.name) && tab.name !== "server"
1698 // && Tabview.getCurrentTab().name.toLowerCase() !== tab.name
1699 //) {
1700 // tab.highlight();
1701 //}
1702
1703 return event;
1704 }
1705 },
1706
1707
1708
1709 {
1710 //Following method taken from: http://snipplr.com/view/13533/convert-text-urls-into-links/
1711 name: "linkify_plain",
1712 onaddmsg: function (event, opts) {
1713 if (!event.msg) {
1714 return event;
1715 }
1716
1717 event.msg = event.msg.replace(/((https?\:\/\/|ftp\:\/\/)|(www\.))(\S+)(\w{2,4})(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/gi, function (url) {
1718 var nice;
1719 // If it's any of the supported images in the images plugin, skip it
1720 if (url.match(/(\.jpg|\.jpeg|\.gif|\.bmp|\.png)$/)) {
1721 return url;
1722 }
1723
1724 nice = url;
1725 if (url.match('^https?:\/\/')) {
1726 //nice = nice.replace(/^https?:\/\//i,'')
1727 nice = url; // Shutting up JSLint...
1728 } else {
1729 url = 'http://' + url;
1730 }
1731
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>';
1734 });
1735
1736 return event;
1737 }
1738 },
1739
1740 {
1741 name: "lftobr",
1742 onaddmsg: function (event, opts) {
1743 if (!event.msg) {
1744 return event;
1745 }
1746
1747 event.msg = event.msg.replace(/\n/gi, function (txt) {
1748 return '<br/>';
1749 });
1750
1751 return event;
1752 }
1753 },
1754
1755
1756 /*
1757 * Disabled due to many websites closing kiwi with iframe busting
1758 {
1759 name: "inBrowser",
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);
1764 },
1765
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);
1771 },
1772
1773
1774
1775 mouseover: function (e) {
1776 var a = $(this),
1777 tt = $('.tt', a),
1778 tooltip;
1779
1780 if (tt.text() === '') {
1781 tooltip = $('<a class="link_ext_browser">Open in Kiwi..</a>');
1782 tt.append(tooltip);
1783 }
1784
1785 tt.css('top', -tt.outerHeight() + 'px');
1786 tt.css('left', (a.outerWidth() / 2) - (tt.outerWidth() / 2));
1787 },
1788
1789 mouseout: function (e) {
1790 var a = $(this),
1791 tt = $('.tt', a);
1792 },
1793
1794 mouseclick: function (e) {
1795 var a = $(this),
1796 t;
1797
1798 switch (e.target.className) {
1799 case 'link_ext':
1800 case 'link_img_a':
1801 return true;
1802 //break;
1803 case 'link_ext_browser':
1804 t = new Utilityview('Browser');
1805 t.topic = a.attr('href');
1806
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);
1810 t.show();
1811 break;
1812 }
1813 return false;
1814
1815 }
1816 },
1817 */
1818
1819 {
1820 name: "nick_colour",
1821 onaddmsg: function (event, opts) {
1822 if (!event.msg) {
1823 return event;
1824 }
1825
1826 //if (typeof kiwi.front.tabviews[event.tabview].nick_colours === 'undefined') {
1827 // kiwi.front.tabviews[event.tabview].nick_colours = {};
1828 //}
1829
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();
1832 //}
1833
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>';
1837
1838 return event;
1839 },
1840
1841
1842
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 + '%)';
1848 },
1849
1850
1851 rand: function (min, max) {
1852 return parseInt(Math.random() * (max - min + 1), 10) + min;
1853 }
1854 },
1855
1856 {
1857 name: "kiwitest",
1858 oninit: function (event, opts) {
1859 console.log('registering namespace');
1860 $(gateway).bind("kiwi.lol.browser", function (e, data) {
1861 console.log('YAY kiwitest');
1862 console.log(data);
1863 });
1864 }
1865 }
1866 ];
1867
1868
1869
1870
1871
1872
1873
1874 /**
1875 * @namespace
1876 */
1877 kiwi.plugs = {};
1878 /**
1879 * Loaded plugins
1880 */
1881 kiwi.plugs.loaded = {};
1882 /**
1883 * Load a plugin
1884 * @param {Object} plugin The plugin to be loaded
1885 * @returns {Boolean} True on success, false on failure
1886 */
1887 kiwi.plugs.loadPlugin = function (plugin) {
1888 var plugin_ret;
1889 if (typeof plugin.name !== 'string') {
1890 return false;
1891 }
1892
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);
1897 }
1898 kiwi.plugs.run('init', {}, {run_only: plugin_ret.plugin.name});
1899
1900 return true;
1901 };
1902
1903 /**
1904 * Unload a plugin
1905 * @param {String} plugin_name The name of the plugin to unload
1906 */
1907 kiwi.plugs.unloadPlugin = function (plugin_name) {
1908 if (typeof kiwi.plugs.loaded[plugin_name] !== 'object') {
1909 return;
1910 }
1911
1912 kiwi.plugs.run('unload', {}, {run_only: plugin_name});
1913 delete kiwi.plugs.loaded[plugin_name];
1914 };
1915
1916
1917
1918 /**
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
1924 */
1925 kiwi.plugs.run = function (event_name, event_data, opts) {
1926 var ret = event_data,
1927 ret_tmp,
1928 plugin_name;
1929
1930 // Set some defaults if not provided
1931 event_data = (typeof event_data === 'undefined') ? {} : event_data;
1932 opts = (typeof opts === 'undefined') ? {} : opts;
1933
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) {
1937 continue;
1938 }
1939
1940 if (typeof kiwi.plugs.loaded[plugin_name]['on' + event_name] === 'function') {
1941 try {
1942 ret_tmp = kiwi.plugs.loaded[plugin_name]['on' + event_name](ret, opts);
1943 if (ret_tmp === null) {
1944 return null;
1945 }
1946 ret = ret_tmp;
1947
1948 if (typeof ret.event_bubbles === 'boolean' && ret.event_bubbles === false) {
1949 delete ret.event_bubbles;
1950 return ret;
1951 }
1952 } catch (e) {
1953 }
1954 }
1955 }
1956
1957 return ret;
1958 };
1959
1960
1961
1962 /**
1963 * @constructor
1964 * @param {String} data_namespace The namespace for the data store
1965 */
1966 kiwi.dataStore = function (data_namespace) {
1967 var namespace = data_namespace;
1968
1969 this.get = function (key) {
1970 return $.jStorage.get(data_namespace + '_' + key);
1971 };
1972
1973 this.set = function (key, value) {
1974 return $.jStorage.set(data_namespace + '_' + key, value);
1975 };
1976 };
1977
1978 kiwi.data = new kiwi.dataStore('kiwi');
1979
1980
1981
1982
1983 /*
1984 * jQuery jStorage plugin
1985 * https://github.com/andris9/jStorage/
1986 */
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.$);
1988
1989
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 */
1991 /*global kiwi */
1992
1993 kiwi.view.MemberList = Backbone.View.extend({
1994 tagName: "ul",
1995 events: {
1996 "click .nick": "nickClick"
1997 },
1998 initialize: function (options) {
1999 this.model.bind('all', this.render, this);
2000 $(this.el).appendTo('#memberlists');
2001 },
2002 render: function () {
2003 var $this = $(this.el);
2004 $this.empty();
2005 this.model.forEach(function (member) {
2006 $('<li><a class="nick"><span class="prefix">' + member.get("prefix") + '</span>' + member.get("nick") + '</a></li>')
2007 .appendTo($this)
2008 .data('member', member);
2009 });
2010 },
2011 nickClick: function (x) {
2012 var target = $(x.currentTarget).parent('li'),
2013 member = target.data('member'),
2014 userbox = new kiwi.view.UserBox();
2015
2016 userbox.member = member;
2017 $('.userbox', this.$el).remove();
2018 target.append(userbox.$el);
2019 },
2020 show: function () {
2021 $('#memberlists').children().removeClass('active');
2022 $(this.el).addClass('active');
2023 }
2024 });
2025
2026
2027 kiwi.view.UserBox = Backbone.View.extend({
2028 // Member this userbox is relating to
2029 member: {},
2030
2031 events: {
2032 'click .query': 'queryClick',
2033 'click .info': 'infoClick'
2034 },
2035
2036 initialize: function () {
2037 this.$el = $($('#tmpl_userbox').html());
2038 },
2039
2040 queryClick: function (event) {
2041 var panel = new kiwi.model.Channel({name: this.member.get('nick')});
2042 kiwi.app.panels.add(panel);
2043 panel.view.show();
2044 },
2045
2046 infoClick: function (event) {
2047 kiwi.gateway.raw('WHOIS ' + this.member.get('nick'));
2048 }
2049 });
2050
2051
2052 kiwi.view.ServerSelect = Backbone.View.extend({
2053 events: {
2054 'submit form': 'submitLogin',
2055 'click .show_more': 'showMore'
2056 },
2057
2058 initialize: function () {
2059 this.$el = $($('#tmpl_server_select').html());
2060
2061 kiwi.gateway.bind('onconnect', this.networkConnected, this);
2062 kiwi.gateway.bind('connecting', this.networkConnecting, this);
2063 },
2064
2065 submitLogin: function (event) {
2066 var values = {
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()
2073 };
2074
2075 this.trigger('server_connect', values);
2076 return false;
2077 },
2078
2079 showMore: function (event) {
2080 $('.more', this.$el).slideDown('fast');
2081 },
2082
2083 populateFields: function (defaults) {
2084 var nick, server, channel;
2085
2086 defaults = defaults || {};
2087
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 || '';
2094
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);
2101 },
2102
2103 hide: function () {
2104 this.$el.slideUp();
2105 },
2106
2107 show: function () {
2108 this.$el.show();
2109 $('.nick', this.$el).focus();
2110 },
2111
2112 setStatus: function (text, class_name) {
2113 $('.status', this.$el)
2114 .text(text)
2115 .attr('class', 'status')
2116 .addClass(class_name)
2117 .show();
2118 },
2119 clearStatus: function () {
2120 $('.status', this.$el).hide();
2121 },
2122
2123 networkConnected: function (event) {
2124 this.setStatus('Connected :)', 'ok');
2125 $('form', this.$el).hide();
2126 },
2127
2128 networkConnecting: function (event) {
2129 this.setStatus('Connecting..', 'ok');
2130 },
2131
2132 showError: function (event) {
2133 this.setStatus('Error connecting', 'error');
2134 $('form', this.$el).show();
2135 }
2136 });
2137
2138
2139 kiwi.view.Panel = Backbone.View.extend({
2140 tagName: "div",
2141 className: "messages",
2142 events: {
2143 "click .chan": "chanClick"
2144 },
2145
2146 // The container this panel is within
2147 $container: null,
2148
2149 initialize: function (options) {
2150 this.initializePanel(options);
2151 },
2152
2153 initializePanel: function (options) {
2154 this.$el.css('display', 'none');
2155
2156 // Containing element for this panel
2157 if (options.container) {
2158 this.$container = $(options.container);
2159 } else {
2160 this.$container = $('#panels .container1');
2161 }
2162
2163 this.$el.appendTo(this.$container);
2164
2165 this.model.bind('msg', this.newMsg, this);
2166 this.msg_count = 0;
2167
2168 this.model.set({"view": this}, {"silent": true});
2169 },
2170
2171 render: function () {
2172 this.$el.empty();
2173 this.model.get("backscroll").forEach(this.newMsg);
2174 },
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,
2178 nick_colour_hex;
2179
2180 // Escape any HTML that may be in here
2181 msg.msg = $('<div />').text(msg.msg).html();
2182
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>';
2187 });
2188
2189
2190 // Make links clickable
2191 msg.msg = msg.msg.replace(/((https?\:\/\/|ftp\:\/\/)|(www\.))(\S+)(\w{2,4})(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]*))?/gi, function (url) {
2192 var nice;
2193
2194 // Add the http is no protoocol was found
2195 if (url.match(/^www\./)) {
2196 url = 'http://' + url;
2197 }
2198
2199 nice = url;
2200 if (nice.length > 100) {
2201 nice = nice.substr(0, 100) + '...';
2202 }
2203
2204 return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url + '">' + nice + '</a>';
2205 });
2206
2207
2208 // Convert IRC formatting into HTML formatting
2209 msg.msg = formatIRCMsg(msg.msg);
2210
2211
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;
2215
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);
2219
2220 return '#' + rgb.toString(16);
2221 })(msg.nick);
2222
2223 msg.nick_style = 'color:' + nick_colour_hex + ';';
2224
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));
2228
2229 this.scrollToBottom();
2230
2231 // Make sure our DOM isn't getting too large (Acts as scrollback)
2232 this.msg_count++;
2233 if (this.msg_count > 250) {
2234 $('.msg:first', this.div).remove();
2235 this.msg_count--;
2236 }
2237 },
2238 chanClick: function (x) {
2239 kiwi.gateway.join($(x.srcElement).text());
2240 },
2241 show: function () {
2242 var $this = this.$el;
2243
2244 // Hide all other panels and show this one
2245 this.$container.children().css('display', 'none');
2246 $this.css('display', 'block');
2247
2248 // Show this panels memberlist
2249 var members = this.model.get("members");
2250 if (members) {
2251 members.view.show();
2252 this.$container.parent().css('right', '200px');
2253 } else {
2254 // Memberlist not found for this panel, hide any active ones
2255 $('#memberlists').children().removeClass('active');
2256 this.$container.parent().css('right', '0');
2257 }
2258
2259 this.scrollToBottom();
2260
2261 this.trigger('active', this.model);
2262 kiwi.app.panels.trigger('active', this.model);
2263 },
2264
2265
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;
2270 }
2271 });
2272
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);
2277 },
2278
2279 topic: function (topic) {
2280 if (typeof topic !== 'string' || !topic) {
2281 topic = this.model.get("topic");
2282 }
2283
2284 this.model.addMsg('', '=== Topic for ' + this.model.get('name') + ' is: ' + topic, 'topic');
2285
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"));
2289 }
2290 }
2291 });
2292
2293 // Model for this = kiwi.model.PanelList
2294 kiwi.view.Tabs = Backbone.View.extend({
2295 events: {
2296 "click li": "tabClick",
2297 'click li img': 'partClick'
2298 },
2299
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);
2304
2305 this.model.on('active', this.panelActive, this);
2306
2307 kiwi.gateway.on('change:name', function (gateway, new_val) {
2308 $('span', this.model.server.tab).text(new_val);
2309 }, this);
2310 },
2311 render: function () {
2312 var that = this;
2313
2314 this.$el.empty();
2315
2316 // Add the server tab first
2317 this.model.server.tab
2318 .data('panel_id', this.model.server.cid)
2319 .appendTo(this.$el);
2320
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;
2325
2326 panel.tab
2327 .data('panel_id', panel.cid)
2328 .appendTo(this.$el);
2329 });
2330 },
2331
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);
2337 },
2338 panelRemoved: function (panel) {
2339 panel.tab.remove();
2340 delete panel.tab;
2341 },
2342
2343 panelActive: function (panel) {
2344 // Remove any existing tabs or part images
2345 $('img', this.$el).remove();
2346 this.$el.children().removeClass('active');
2347
2348 panel.tab.addClass('active');
2349 panel.tab.append('<img src="img/redcross.png" />');
2350 },
2351
2352 tabClick: function (e) {
2353 var tab = $(e.currentTarget);
2354
2355 var panel = this.model.getByCid(tab.data('panel_id'));
2356 if (!panel) {
2357 // A panel wasn't found for this tab... wadda fuck
2358 return;
2359 }
2360
2361 panel.view.show();
2362 },
2363
2364 partClick: function (e) {
2365 var tab = $(e.currentTarget).parent();
2366 var panel = this.model.getByCid(tab.data('panel_id'));
2367
2368 // Only need to part if it's a channel
2369 if (panel.isChannel()) {
2370 kiwi.gateway.part(panel.get('name'));
2371 } else {
2372 panel.close();
2373 }
2374 },
2375
2376 next: function () {
2377 var next = kiwi.app.panels.active.tab.next();
2378 if (!next.length) next = $('li:first', this.$el);
2379
2380 next.click();
2381 },
2382 prev: function () {
2383 var prev = kiwi.app.panels.active.tab.prev();
2384 if (!prev.length) prev = $('li:last', this.$el);
2385
2386 prev.click();
2387 }
2388 });
2389
2390
2391
2392 kiwi.view.TopicBar = Backbone.View.extend({
2393 events: {
2394 'keydown input': 'process'
2395 },
2396
2397 initialize: function () {
2398 kiwi.app.panels.bind('active', function (active_panel) {
2399 this.setCurrentTopic(active_panel.get('topic'));
2400 }, this);
2401 },
2402
2403 process: function (ev) {
2404 var inp = $(ev.currentTarget),
2405 inp_val = inp.val();
2406
2407 if (ev.keyCode !== 13) return;
2408
2409 if (kiwi.app.panels.active.isChannel()) {
2410 kiwi.gateway.topic(kiwi.app.panels.active.get('name'), inp_val);
2411 }
2412 },
2413
2414 setCurrentTopic: function (new_topic) {
2415 new_topic = new_topic || '';
2416
2417 // We only want a plain text version
2418 new_topic = $('<div>').html(formatIRCMsg(new_topic));
2419 $('input', this.$el).val(new_topic.text());
2420 }
2421 });
2422
2423
2424
2425 kiwi.view.ControlBox = Backbone.View.extend({
2426 buffer: [], // Stores previously run commands
2427 buffer_pos: 0, // The current position in the buffer
2428
2429 // Hold tab autocomplete data
2430 tabcomplete: {active: false, data: [], prefix: ''},
2431
2432 events: {
2433 'keydown input': 'process'
2434 },
2435
2436 initialize: function () {
2437 var that = this;
2438
2439 kiwi.gateway.bind('change:nick', function () {
2440 $('.nick', that.$el).text(this.get('nick'));
2441 });
2442 },
2443
2444 process: function (ev) {
2445 var that = this,
2446 inp = $(ev.currentTarget),
2447 inp_val = inp.val(),
2448 meta;
2449
2450 if (navigator.appVersion.indexOf("Mac") !== -1) {
2451 meta = ev.ctrlKey;
2452 } else {
2453 meta = ev.altKey;
2454 }
2455
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 = '';
2461 }
2462
2463 switch (true) {
2464 case (ev.keyCode === 13): // return
2465 inp_val = inp_val.trim();
2466
2467 if (inp_val) {
2468 this.processInput(inp.val());
2469
2470 this.buffer.push(inp.val());
2471 this.buffer_pos = this.buffer.length;
2472 }
2473
2474 inp.val('');
2475
2476 break;
2477
2478 case (ev.keyCode === 38): // up
2479 if (this.buffer_pos > 0) {
2480 this.buffer_pos--;
2481 inp.val(this.buffer[this.buffer_pos]);
2482 }
2483 break;
2484
2485 case (ev.keyCode === 40): // down
2486 if (this.buffer_pos < this.buffer.length) {
2487 this.buffer_pos++;
2488 inp.val(this.buffer[this.buffer_pos]);
2489 }
2490 break;
2491
2492 case (ev.keyCode === 37 && meta): // left
2493 kiwi.app.panels.view.prev();
2494 return false;
2495
2496 case (ev.keyCode === 39 && meta): // right
2497 kiwi.app.panels.view.next();
2498 return false;
2499
2500 case (ev.keyCode === 9): // tab
2501 this.tabcomplete.active = true;
2502 if (_.isEqual(this.tabcomplete.data, [])) {
2503 // Get possible autocompletions
2504 var ac_data = [];
2505 $.each(kiwi.app.panels.active.get('members').models, function (i, member) {
2506 if (!member) return;
2507 ac_data.push(member.get('nick'));
2508 });
2509 ac_data = _.sortBy(ac_data, function (nick) {
2510 return nick;
2511 });
2512 this.tabcomplete.data = ac_data;
2513 }
2514
2515 if (inp_val[inp[0].selectionStart - 1] === ' ') {
2516 return false;
2517 }
2518
2519 (function () {
2520 var tokens = inp_val.substring(0, inp[0].selectionStart).split(' '),
2521 val,
2522 p1,
2523 newnick,
2524 range,
2525 nick = tokens[tokens.length - 1];
2526 if (this.tabcomplete.prefix === '') {
2527 this.tabcomplete.prefix = nick;
2528 }
2529
2530 this.tabcomplete.data = _.select(this.tabcomplete.data, function (n) {
2531 return (n.toLowerCase().indexOf(that.tabcomplete.prefix.toLowerCase()) === 0);
2532 });
2533
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);
2539 val += newnick;
2540 val += inp_val.substr(inp[0].selectionStart);
2541 inp.val(val);
2542
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);
2550 range.select();
2551 }
2552 }
2553 }).apply(this);
2554 return false;
2555 }
2556 },
2557
2558
2559 processInput: function (command_raw) {
2560 var command,
2561 params = command_raw.split(' ');
2562
2563 // Extract the command and parameters
2564 if (params[0][0] === '/') {
2565 command = params[0].substr(1).toLowerCase();
2566 params = params.splice(1);
2567 } else {
2568 // Default command
2569 command = 'msg';
2570 params.unshift(kiwi.app.panels.active.get('name'));
2571 }
2572
2573 // Trigger the command events
2574 this.trigger('command', {command: command, params: params});
2575 this.trigger('command_' + command, {command: command, params: params});
2576
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});
2581 }
2582 }
2583 });
2584
2585
2586
2587
2588 kiwi.view.StatusMessage = Backbone.View.extend({
2589 /* Timer for hiding the message */
2590 tmr: null,
2591
2592 initialize: function () {
2593 this.$el.hide();
2594 },
2595
2596 text: function (text, opt) {
2597 // Defaults
2598 opt = opt || {};
2599 opt.type = opt.type || '';
2600
2601 this.$el.text(text).attr('class', opt.type);
2602 this.$el.slideDown(kiwi.app.view.doLayout);
2603
2604 if (opt.timeout) this.doTimeout(opt.timeout);
2605 },
2606
2607 html: function (html, opt) {
2608 // Defaults
2609 opt = opt || {};
2610 opt.type = opt.type || '';
2611
2612 this.$el.html(text).attr('class', opt.type);
2613 this.$el.slideDown(kiwi.app.view.doLayout);
2614
2615 if (opt.timeout) this.doTimeout(opt.timeout);
2616 },
2617
2618 hide: function () {
2619 this.$el.slideUp(kiwi.app.view.doLayout);
2620 },
2621
2622 doTimeout: function (length) {
2623 if (this.tmr) clearTimeout(this.tmr);
2624 var that = this;
2625 this.tmr = setTimeout(function () { that.hide(); }, length);
2626 }
2627 });
2628
2629
2630
2631
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);
2637
2638 this.doLayout();
2639
2640 $(document).keydown(this.setKeyFocus);
2641 },
2642
2643
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) {
2648 return;
2649 }
2650
2651 // If we're typing into an input box somewhere, ignore
2652 if (ev.target.tagName.toLowerCase() === 'input') {
2653 return;
2654 }
2655
2656 $('#controlbox .inp').focus();
2657 },
2658
2659
2660 doLayout: function () {
2661 var el_panels = $('#panels');
2662 var el_memberlists = $('#memberlists');
2663 var el_toolbar = $('#toolbar');
2664 var el_controlbox = $('#controlbox');
2665
2666 var css_heights = {
2667 top: el_toolbar.outerHeight(true),
2668 bottom: el_controlbox.outerHeight(true)
2669 };
2670
2671 el_panels.css(css_heights);
2672 el_memberlists.css(css_heights);
2673 },
2674
2675
2676 barsHide: function (instant) {
2677 var that = this;
2678
2679 if (!instant) {
2680 $('#toolbar').slideUp();
2681 $('#controlbox').slideUp(function () { that.doLayout(); });
2682 } else {
2683 $('#toolbar').slideUp(0);
2684 $('#controlbox').slideUp(0);
2685 }
2686 },
2687
2688 barsShow: function (instant) {
2689 var that = this;
2690
2691 if (!instant) {
2692 $('#toolbar').slideDown();
2693 $('#controlbox').slideDown(function () { that.doLayout(); });
2694 } else {
2695 $('#toolbar').slideDown(0);
2696 $('#controlbox').slideDown(0);
2697 this.doLayout();
2698 }
2699 }
2700 });
2701
2702
2703
2704 })(window);