df3ca247312856484cb00f0f04b9631a25455446
[KiwiIRC.git] / client / assets / dev / view.js
1 /*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 */
2 /*global kiwi */
3
4 _kiwi.view.MemberList = Backbone.View.extend({
5 tagName: "ul",
6 events: {
7 "click .nick": "nickClick"
8 },
9 initialize: function (options) {
10 this.model.bind('all', this.render, this);
11 $(this.el).appendTo('#memberlists');
12 },
13 render: function () {
14 var $this = $(this.el);
15 $this.empty();
16 this.model.forEach(function (member) {
17 var prefix_css_class = (member.get('modes') || []).join(' ');
18 $('<li class="mode ' + prefix_css_class + '"><a class="nick"><span class="prefix">' + member.get("prefix") + '</span>' + member.get("nick") + '</a></li>')
19 .appendTo($this)
20 .data('member', member);
21 });
22 },
23 nickClick: function (x) {
24 var $target = $(x.currentTarget).parent('li'),
25 member = $target.data('member'),
26 userbox;
27
28 // If the userbox already exists here, hide it
29 if ($target.find('.userbox').length > 0) {
30 $('.userbox', this.$el).remove();
31 return;
32 }
33
34 userbox = new _kiwi.view.UserBox();
35 userbox.member = member;
36 userbox.channel = this.model.channel;
37
38 // Remove any existing userboxes
39 $('.userbox', this.$el).remove();
40
41 if (!this.model.getByNick(_kiwi.gateway.get('nick')).get('is_op')) {
42 userbox.$el.children('.if_op').remove();
43 }
44 $target.append(userbox.$el);
45 },
46 show: function () {
47 $('#memberlists').children().removeClass('active');
48 $(this.el).addClass('active');
49 }
50 });
51
52
53
54 _kiwi.view.UserBox = Backbone.View.extend({
55 events: {
56 'click .query': 'queryClick',
57 'click .info': 'infoClick',
58 'click .slap': 'slapClick',
59 'click .op': 'opClick',
60 'click .deop': 'deopClick',
61 'click .voice': 'voiceClick',
62 'click .devoice': 'devoiceClick',
63 'click .kick': 'kickClick',
64 'click .ban': 'banClick'
65 },
66
67 initialize: function () {
68 this.$el = $($('#tmpl_userbox').html());
69 },
70
71 queryClick: function (event) {
72 var panel = new _kiwi.model.Query({name: this.member.get('nick')});
73 _kiwi.app.panels.add(panel);
74 panel.view.show();
75 },
76
77 infoClick: function (event) {
78 _kiwi.app.controlbox.processInput('/whois ' + this.member.get('nick'));
79 },
80
81 slapClick: function (event) {
82 _kiwi.app.controlbox.processInput('/slap ' + this.member.get('nick'));
83 },
84
85 opClick: function (event) {
86 _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +o ' + this.member.get('nick'));
87 },
88
89 deopClick: function (event) {
90 _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' -o ' + this.member.get('nick'));
91 },
92
93 voiceClick: function (event) {
94 _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +v ' + this.member.get('nick'));
95 },
96
97 devoiceClick: function (event) {
98 _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' -v ' + this.member.get('nick'));
99 },
100
101 kickClick: function (event) {
102 // TODO: Enable the use of a custom kick message
103 _kiwi.app.controlbox.processInput('/kick ' + this.member.get('nick') + ' Bye!');
104 },
105
106 banClick: function (event) {
107 // TODO: Set ban on host, not just on nick
108 _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +b ' + this.member.get('nick') + '!*');
109 }
110 });
111
112 _kiwi.view.NickChangeBox = Backbone.View.extend({
113 events: {
114 'submit': 'changeNick',
115 'click .cancel': 'close'
116 },
117
118 initialize: function () {
119 this.$el = $($('#tmpl_nickchange').html());
120 },
121
122 render: function () {
123 // Add the UI component and give it focus
124 _kiwi.app.controlbox.$el.prepend(this.$el);
125 this.$el.find('input').focus();
126
127 this.$el.css('bottom', _kiwi.app.controlbox.$el.outerHeight(true));
128 },
129
130 close: function () {
131 this.$el.remove();
132
133 },
134
135 changeNick: function (event) {
136 var that = this;
137 _kiwi.gateway.changeNick(this.$el.find('input').val(), function (err, val) {
138 that.close();
139 });
140 return false;
141 }
142 });
143
144 _kiwi.view.ServerSelect = function () {
145 // Are currently showing all the controlls or just a nick_change box?
146 var state = 'all';
147
148 var model = Backbone.View.extend({
149 events: {
150 'submit form': 'submitForm',
151 'click .show_more': 'showMore',
152 'change .have_pass input': 'showPass'
153 },
154
155 initialize: function () {
156 var that = this;
157
158 this.$el = $($('#tmpl_server_select').html());
159
160 // Remove the 'more' link if the server has disabled server changing
161 if (_kiwi.app.server_settings && _kiwi.app.server_settings.connection) {
162 if (!_kiwi.app.server_settings.connection.allow_change) {
163 this.$el.find('.show_more').remove();
164 this.$el.addClass('single_server');
165 }
166 }
167
168
169 _kiwi.gateway.bind('onconnect', this.networkConnected, this);
170 _kiwi.gateway.bind('connecting', this.networkConnecting, this);
171
172 _kiwi.gateway.bind('onirc_error', function (data) {
173 $('button', this.$el).attr('disabled', null);
174
175 if (data.error == 'nickname_in_use') {
176 this.setStatus('Nickname already taken');
177 this.show('nick_change');
178 }
179
180 if (data.error == 'password_mismatch') {
181 this.setStatus('Incorrect Password');
182 this.show('nick_change');
183 that.$el.find('.password').select();
184 }
185 }, this);
186 },
187
188 submitForm: function (event) {
189 event.preventDefault();
190
191 // Make sure a nick is chosen
192 if (!$('input.nick', this.$el).val().trim()) {
193 this.setStatus('Select a nickname first!');
194 $('input.nick', this.$el).select();
195 return;
196 }
197
198 if (state === 'nick_change') {
199 this.submitNickChange(event);
200 } else {
201 this.submitLogin(event);
202 }
203
204 $('button', this.$el).attr('disabled', 1);
205 return;
206 },
207
208 submitLogin: function (event) {
209 // If submitting is disabled, don't do anything
210 if ($('button', this.$el).attr('disabled')) return;
211
212 var values = {
213 nick: $('input.nick', this.$el).val(),
214 server: $('input.server', this.$el).val(),
215 port: $('input.port', this.$el).val(),
216 ssl: $('input.ssl', this.$el).prop('checked'),
217 password: $('input.password', this.$el).val(),
218 channel: $('input.channel', this.$el).val(),
219 channel_key: $('input.channel_key', this.$el).val()
220 };
221
222 this.trigger('server_connect', values);
223 },
224
225 submitNickChange: function (event) {
226 _kiwi.gateway.changeNick($('input.nick', this.$el).val());
227 this.networkConnecting();
228 },
229
230 showPass: function (event) {
231 if (this.$el.find('tr.have_pass input').is(':checked')) {
232 this.$el.find('tr.pass').show().find('input').focus();
233 } else {
234 this.$el.find('tr.pass').hide().find('input').val('');
235 }
236 },
237
238 showMore: function (event) {
239 $('.more', this.$el).slideDown('fast');
240 $('input.server', this.$el).select();
241 },
242
243 populateFields: function (defaults) {
244 var nick, server, port, channel, channel_key, ssl, password;
245
246 defaults = defaults || {};
247
248 nick = defaults.nick || '';
249 server = defaults.server || '';
250 port = defaults.port || 6667;
251 ssl = defaults.ssl || 0;
252 password = defaults.password || '';
253 channel = defaults.channel || '';
254 channel_key = defaults.channel_key || '';
255
256 $('input.nick', this.$el).val(nick);
257 $('input.server', this.$el).val(server);
258 $('input.port', this.$el).val(port);
259 $('input.ssl', this.$el).prop('checked', ssl);
260 $('input.password', this.$el).val(password);
261 $('input.channel', this.$el).val(channel);
262 $('input.channel_key', this.$el).val(channel_key);
263 },
264
265 hide: function () {
266 this.$el.slideUp();
267 },
268
269 show: function (new_state) {
270 new_state = new_state || 'all';
271
272 this.$el.show();
273
274 if (new_state === 'all') {
275 $('.show_more', this.$el).show();
276
277 } else if (new_state === 'more') {
278 $('.more', this.$el).slideDown('fast');
279
280 } else if (new_state === 'nick_change') {
281 $('.more', this.$el).hide();
282 $('.show_more', this.$el).hide();
283 $('input.nick', this.$el).select();
284 }
285
286 state = new_state;
287 },
288
289 setStatus: function (text, class_name) {
290 $('.status', this.$el)
291 .text(text)
292 .attr('class', 'status')
293 .addClass(class_name||'')
294 .show();
295 },
296 clearStatus: function () {
297 $('.status', this.$el).hide();
298 },
299
300 networkConnected: function (event) {
301 this.setStatus('Connected :)', 'ok');
302 $('form', this.$el).hide();
303 },
304
305 networkConnecting: function (event) {
306 this.setStatus('Connecting..', 'ok');
307 },
308
309 showError: function (event) {
310 this.setStatus('Error connecting', 'error');
311 $('button', this.$el).attr('disabled', null);
312 this.show();
313 }
314 });
315
316
317 return new model(arguments);
318 };
319
320
321 _kiwi.view.Panel = Backbone.View.extend({
322 tagName: "div",
323 className: "messages",
324 events: {
325 "click .chan": "chanClick",
326 'click .media .open': 'mediaClick',
327 'mouseenter .msg .nick': 'msgEnter',
328 'mouseleave .msg .nick': 'msgLeave'
329 },
330
331 initialize: function (options) {
332 this.initializePanel(options);
333 },
334
335 initializePanel: function (options) {
336 this.$el.css('display', 'none');
337 options = options || {};
338
339 // Containing element for this panel
340 if (options.container) {
341 this.$container = $(options.container);
342 } else {
343 this.$container = $('#panels .container1');
344 }
345
346 this.$el.appendTo(this.$container);
347
348 this.alert_level = 0;
349
350 this.model.bind('msg', this.newMsg, this);
351 this.msg_count = 0;
352
353 this.model.set({"view": this}, {"silent": true});
354 },
355
356 render: function () {
357 var that = this;
358
359 this.$el.empty();
360 _.each(this.model.get('scrollback'), function (msg) {
361 that.newMsg(msg);
362 });
363 },
364
365 newMsg: function (msg) {
366 var re, line_msg, $this = this.$el,
367 nick_colour_hex, nick_hex, is_highlight, msg_css_classes = '';
368
369 // Nick highlight detecting
370 if ((new RegExp('\\b' + _kiwi.gateway.get('nick') + '\\b', 'i')).test(msg.msg)) {
371 is_highlight = true;
372 msg_css_classes += ' highlight';
373 }
374
375 // Escape any HTML that may be in here
376 msg.msg = $('<div />').text(msg.msg).html();
377
378 // Make the channels clickable
379 re = new RegExp('(?:^|\\s)([' + _kiwi.gateway.get('channel_prefix') + '][^ ,.\\007]+)', 'g');
380 msg.msg = msg.msg.replace(re, function (match) {
381 return '<a class="chan" data-channel="' + match.trim() + '">' + match + '</a>';
382 });
383
384
385 // Parse any links found
386 msg.msg = msg.msg.replace(/(([A-Za-z0-9\-]+\:\/\/)|(www\.))([\w.\-]+)([a-zA-Z]{2,6})(:[0-9]+)?(\/[\w#!:.?$'()[\]*,;~+=&%@!\-\/]*)?/gi, function (url) {
387 var nice = url,
388 extra_html = '';
389
390 // Add the http if no protoocol was found
391 if (url.match(/^www\./)) {
392 url = 'http://' + url;
393 }
394
395 // Shorten the displayed URL if it's going to be too long
396 if (nice.length > 100) {
397 nice = nice.substr(0, 100) + '...';
398 }
399
400 // Get any media HTML if supported
401 extra_html = _kiwi.view.MediaMessage.buildHtml(url);
402
403 // Make the link clickable
404 return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url + '">' + nice + '</a> ' + extra_html;
405 });
406
407
408 // Convert IRC formatting into HTML formatting
409 msg.msg = formatIRCMsg(msg.msg);
410
411
412 // Add some colours to the nick (Method based on IRSSIs nickcolor.pl)
413 nick_colour_hex = (function (nick) {
414 var nick_int = 0, rgb;
415
416 _.map(nick.split(''), function (i) { nick_int += i.charCodeAt(0); });
417 rgb = hsl2rgb(nick_int % 255, 70, 35);
418 rgb = rgb[2] | (rgb[1] << 8) | (rgb[0] << 16);
419
420 return '#' + rgb.toString(16);
421 })(msg.nick);
422
423 msg.nick_style = 'color:' + nick_colour_hex + ';';
424
425 // Generate a hex string from the nick to be used as a CSS class name
426 nick_hex = msg.nick_css_class = '';
427 if (msg.nick) {
428 _.map(msg.nick.split(''), function (char) {
429 nick_hex += char.charCodeAt(0).toString(16);
430 });
431 msg_css_classes += ' nick_' + nick_hex;
432 }
433
434 // Build up and add the line
435 msg.msg_css_classes = msg_css_classes;
436 line_msg = '<div class="msg <%= type %> <%= msg_css_classes %>"><div class="time"><%- time %></div><div class="nick" style="<%= nick_style %>"><%- nick %></div><div class="text" style="<%= style %>"><%= msg %> </div></div>';
437 $this.append(_.template(line_msg, msg));
438
439 // Activity/alerts based on the type of new message
440 if (msg.type.match(/^action /)) {
441 this.alert('action');
442
443 } else if (is_highlight) {
444 _kiwi.app.view.alertWindow('* People are talking!');
445 _kiwi.app.view.playSound('highlight');
446 this.alert('highlight');
447
448 } else {
449 // If this is the active panel, send an alert out
450 if (this.model.isActive()) {
451 _kiwi.app.view.alertWindow('* People are talking!');
452 }
453 this.alert('activity');
454 }
455
456 if (this.model.isQuery() && !this.model.isActive()) {
457 _kiwi.app.view.alertWindow('* People are talking!');
458 _kiwi.app.view.playSound('highlight');
459 }
460
461 // Update the activity counters
462 (function () {
463 // Only inrement the counters if we're not the active panel
464 if (this.model.isActive()) return;
465
466 var $act = this.model.tab.find('.activity');
467 $act.text((parseInt($act.text(), 10) || 0) + 1);
468 if ($act.text() === '0') {
469 $act.addClass('zero');
470 } else {
471 $act.removeClass('zero');
472 }
473 }).apply(this);
474
475 this.scrollToBottom();
476
477 // Make sure our DOM isn't getting too large (Acts as scrollback)
478 this.msg_count++;
479 if (this.msg_count > (parseInt(_kiwi.global.settings.get('scrollback'), 10) || 250)) {
480 $('.msg:first', this.$el).remove();
481 this.msg_count--;
482 }
483 },
484 chanClick: function (event) {
485 if (event.target) {
486 _kiwi.gateway.join($(event.target).data('channel'));
487 } else {
488 // IE...
489 _kiwi.gateway.join($(event.srcElement).data('channel'));
490 }
491 },
492
493 mediaClick: function (event) {
494 var $media = $(event.target).parents('.media');
495 var media_message;
496
497 if ($media.data('media')) {
498 media_message = $media.data('media');
499 } else {
500 media_message = new _kiwi.view.MediaMessage({el: $media[0]});
501 $media.data('media', media_message);
502 }
503
504 $media.data('media', media_message);
505
506 media_message.open();
507 },
508
509 // Cursor hovers over a message
510 msgEnter: function (event) {
511 var nick_class;
512
513 // Find a valid class that this element has
514 _.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) {
515 if (css_class.match(/^nick_[a-z0-9]+/i)) {
516 nick_class = css_class;
517 }
518 });
519
520 // If no class was found..
521 if (!nick_class) return;
522
523 $('.'+nick_class).addClass('global_nick_highlight');
524 },
525
526 // Cursor leaves message
527 msgLeave: function (event) {
528 var nick_class;
529
530 // Find a valid class that this element has
531 _.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) {
532 if (css_class.match(/^nick_[a-z0-9]+/i)) {
533 nick_class = css_class;
534 }
535 });
536
537 // If no class was found..
538 if (!nick_class) return;
539
540 $('.'+nick_class).removeClass('global_nick_highlight');
541 },
542
543 show: function () {
544 var $this = this.$el;
545
546 // Hide all other panels and show this one
547 this.$container.children().css('display', 'none');
548 $this.css('display', 'block');
549
550 // Show this panels memberlist
551 var members = this.model.get("members");
552 if (members) {
553 $('#memberlists').removeClass('disabled');
554 members.view.show();
555 } else {
556 // Memberlist not found for this panel, hide any active ones
557 $('#memberlists').addClass('disabled').children().removeClass('active');
558 }
559
560 _kiwi.app.view.doLayout();
561
562 // Remove any alerts and activity counters for this panel
563 this.alert('none');
564 this.model.tab.find('.activity').text('0').addClass('zero');
565
566 this.trigger('active', this.model);
567 _kiwi.app.panels.trigger('active', this.model, _kiwi.app.panels.active);
568
569 this.scrollToBottom(true);
570 },
571
572
573 alert: function (level) {
574 // No need to highlight if this si the active panel
575 if (this.model == _kiwi.app.panels.active) return;
576
577 var types, type_idx;
578 types = ['none', 'action', 'activity', 'highlight'];
579
580 // Default alert level
581 level = level || 'none';
582
583 // If this alert level does not exist, assume clearing current level
584 type_idx = _.indexOf(types, level);
585 if (!type_idx) {
586 level = 'none';
587 type_idx = 0;
588 }
589
590 // Only 'upgrade' the alert. Never down (unless clearing)
591 if (type_idx !== 0 && type_idx <= this.alert_level) {
592 return;
593 }
594
595 // Clear any existing levels
596 this.model.tab.removeClass(function (i, css) {
597 return (css.match(/\balert_\S+/g) || []).join(' ');
598 });
599
600 // Add the new level if there is one
601 if (level !== 'none') {
602 this.model.tab.addClass('alert_' + level);
603 }
604
605 this.alert_level = type_idx;
606 },
607
608
609 // Scroll to the bottom of the panel
610 scrollToBottom: function (force_down) {
611 // If this isn't the active panel, don't scroll
612 if (this.model !== _kiwi.app.panels.active) return;
613
614 // Don't scroll down if we're scrolled up the panel a little
615 if (force_down || this.$container.scrollTop() + this.$container.height() > this.$el.outerHeight() - 150) {
616 this.$container[0].scrollTop = this.$container[0].scrollHeight;
617 }
618 }
619 });
620
621 _kiwi.view.Applet = _kiwi.view.Panel.extend({
622 className: 'applet',
623 initialize: function (options) {
624 this.initializePanel(options);
625 }
626 });
627
628 _kiwi.view.Channel = _kiwi.view.Panel.extend({
629 initialize: function (options) {
630 this.initializePanel(options);
631 this.model.bind('change:topic', this.topic, this);
632
633 // Only show the loader if this is a channel (ie. not a query)
634 if (this.model.isChannel()) {
635 this.$el.append('<div class="initial_loader" style="margin:1em;text-align:center;">Joining channel.. <span class="loader"></span></div>');
636 }
637 },
638
639 // Override the existing newMsg() method to remove the joining channel loader
640 newMsg: function () {
641 this.$el.find('.initial_loader').slideUp(function () {
642 $(this).remove();
643 });
644
645 return this.constructor.__super__.newMsg.apply(this, arguments);
646 },
647
648 topic: function (topic) {
649 if (typeof topic !== 'string' || !topic) {
650 topic = this.model.get("topic");
651 }
652
653 this.model.addMsg('', '== Topic for ' + this.model.get('name') + ' is: ' + topic, 'topic');
654
655 // If this is the active channel then update the topic bar
656 if (_kiwi.app.panels.active === this) {
657 _kiwi.app.topicbar.setCurrentTopic(this.model.get("topic"));
658 }
659 }
660 });
661
662 // Model for this = _kiwi.model.PanelList
663 _kiwi.view.Tabs = Backbone.View.extend({
664 events: {
665 'click li': 'tabClick',
666 'click li .part': 'partClick'
667 },
668
669 initialize: function () {
670 this.model.on("add", this.panelAdded, this);
671 this.model.on("remove", this.panelRemoved, this);
672 this.model.on("reset", this.render, this);
673
674 this.model.on('active', this.panelActive, this);
675
676 this.tabs_applets = $('ul.applets', this.$el);
677 this.tabs_msg = $('ul.channels', this.$el);
678
679 _kiwi.gateway.on('change:name', function (gateway, new_val) {
680 $('span', this.model.server.tab).text(new_val);
681 }, this);
682 },
683 render: function () {
684 var that = this;
685
686 this.tabs_msg.empty();
687
688 // Add the server tab first
689 this.model.server.tab
690 .data('panel_id', this.model.server.cid)
691 .appendTo(this.tabs_msg);
692
693 // Go through each panel adding its tab
694 this.model.forEach(function (panel) {
695 // If this is the server panel, ignore as it's already added
696 if (panel == that.model.server) return;
697
698 panel.tab
699 .data('panel_id', panel.cid)
700 .appendTo(panel.isApplet() ? this.tabs_applets : this.tabs_msg);
701 });
702
703 _kiwi.app.view.doLayout();
704 },
705
706 updateTabTitle: function (panel, new_title) {
707 $('span', panel.tab).text(new_title);
708 },
709
710 panelAdded: function (panel) {
711 // Add a tab to the panel
712 panel.tab = $('<li><span>' + (panel.get('title') || panel.get('name')) + '</span><div class="activity"></div></li>');
713
714 if (panel.isServer()) {
715 panel.tab.addClass('server');
716 panel.tab.addClass('icon-nonexistant');
717 }
718
719 panel.tab.data('panel_id', panel.cid)
720 .appendTo(panel.isApplet() ? this.tabs_applets : this.tabs_msg);
721
722 panel.bind('change:title', this.updateTabTitle);
723 _kiwi.app.view.doLayout();
724 },
725 panelRemoved: function (panel) {
726 panel.tab.remove();
727 delete panel.tab;
728
729 _kiwi.app.view.doLayout();
730 },
731
732 panelActive: function (panel, previously_active_panel) {
733 // Remove any existing tabs or part images
734 $('.part', this.$el).remove();
735 this.tabs_applets.children().removeClass('active');
736 this.tabs_msg.children().removeClass('active');
737
738 panel.tab.addClass('active');
739
740 // Only show the part image on non-server tabs
741 if (!panel.isServer()) {
742 panel.tab.append('<span class="part icon-nonexistant"></span>');
743 }
744 },
745
746 tabClick: function (e) {
747 var tab = $(e.currentTarget);
748
749 var panel = this.model.getByCid(tab.data('panel_id'));
750 if (!panel) {
751 // A panel wasn't found for this tab... wadda fuck
752 return;
753 }
754
755 panel.view.show();
756 },
757
758 partClick: function (e) {
759 var tab = $(e.currentTarget).parent();
760 var panel = this.model.getByCid(tab.data('panel_id'));
761
762 // Only need to part if it's a channel
763 // If the nicklist is empty, we haven't joined the channel as yet
764 if (panel.isChannel() && panel.get('members').models.length > 0) {
765 _kiwi.gateway.part(panel.get('name'));
766 } else {
767 panel.close();
768 }
769 },
770
771 next: function () {
772 var next = _kiwi.app.panels.active.tab.next();
773 if (!next.length) next = $('li:first', this.tabs_msgs);
774
775 next.click();
776 },
777 prev: function () {
778 var prev = _kiwi.app.panels.active.tab.prev();
779 if (!prev.length) prev = $('li:last', this.tabs_msgs);
780
781 prev.click();
782 }
783 });
784
785
786
787 _kiwi.view.TopicBar = Backbone.View.extend({
788 events: {
789 'keydown div': 'process'
790 },
791
792 initialize: function () {
793 _kiwi.app.panels.bind('active', function (active_panel) {
794 // If it's a channel topic, update and make editable
795 if (active_panel.isChannel()) {
796 this.setCurrentTopic(active_panel.get('topic') || '');
797 this.$el.find('div').attr('contentEditable', true);
798
799 } else {
800 // Not a channel topic.. clear and make uneditable
801 this.$el.find('div').attr('contentEditable', false)
802 .text('');
803 }
804 }, this);
805 },
806
807 process: function (ev) {
808 var inp = $(ev.currentTarget),
809 inp_val = inp.text();
810
811 // Only allow topic editing if this is a channel panel
812 if (!_kiwi.app.panels.active.isChannel()) {
813 return false;
814 }
815
816 // If hit return key, update the current topic
817 if (ev.keyCode === 13) {
818 _kiwi.gateway.topic(_kiwi.app.panels.active.get('name'), inp_val);
819 return false;
820 }
821 },
822
823 setCurrentTopic: function (new_topic) {
824 new_topic = new_topic || '';
825
826 // We only want a plain text version
827 $('div', this.$el).html(formatIRCMsg(_.escape(new_topic)));
828 }
829 });
830
831
832
833 _kiwi.view.ControlBox = Backbone.View.extend({
834 events: {
835 'keydown .inp': 'process',
836 'click .nick': 'showNickChange'
837 },
838
839 initialize: function () {
840 var that = this;
841
842 this.buffer = []; // Stores previously run commands
843 this.buffer_pos = 0; // The current position in the buffer
844
845 this.preprocessor = new InputPreProcessor();
846 this.preprocessor.recursive_depth = 5;
847
848 // Hold tab autocomplete data
849 this.tabcomplete = {active: false, data: [], prefix: ''};
850
851 _kiwi.gateway.bind('change:nick', function () {
852 $('.nick', that.$el).text(this.get('nick'));
853 });
854 },
855
856 showNickChange: function (ev) {
857 (new _kiwi.view.NickChangeBox()).render();
858 },
859
860 process: function (ev) {
861 var that = this,
862 inp = $(ev.currentTarget),
863 inp_val = inp.val(),
864 meta;
865
866 if (navigator.appVersion.indexOf("Mac") !== -1) {
867 meta = ev.metaKey;
868 } else {
869 meta = ev.altKey;
870 }
871
872 // If not a tab key, reset the tabcomplete data
873 if (this.tabcomplete.active && ev.keyCode !== 9) {
874 this.tabcomplete.active = false;
875 this.tabcomplete.data = [];
876 this.tabcomplete.prefix = '';
877 }
878
879 switch (true) {
880 case (ev.keyCode === 13): // return
881 inp_val = inp_val.trim();
882
883 if (inp_val) {
884 $.each(inp_val.split('\n'), function (idx, line) {
885 that.processInput(line);
886 });
887
888 this.buffer.push(inp_val);
889 this.buffer_pos = this.buffer.length;
890 }
891
892 inp.val('');
893 return false;
894
895 break;
896
897 case (ev.keyCode === 38): // up
898 if (this.buffer_pos > 0) {
899 this.buffer_pos--;
900 inp.val(this.buffer[this.buffer_pos]);
901 }
902 break;
903
904 case (ev.keyCode === 40): // down
905 if (this.buffer_pos < this.buffer.length) {
906 this.buffer_pos++;
907 inp.val(this.buffer[this.buffer_pos]);
908 }
909 break;
910
911 case (ev.keyCode === 219 && meta): // [ + meta
912 _kiwi.app.panels.view.prev();
913 return false;
914
915 case (ev.keyCode === 221 && meta): // ] + meta
916 _kiwi.app.panels.view.next();
917 return false;
918
919 case (ev.keyCode === 9): // tab
920 this.tabcomplete.active = true;
921 if (_.isEqual(this.tabcomplete.data, [])) {
922 // Get possible autocompletions
923 var ac_data = [];
924 $.each(_kiwi.app.panels.active.get('members').models, function (i, member) {
925 if (!member) return;
926 ac_data.push(member.get('nick'));
927 });
928 ac_data = _.sortBy(ac_data, function (nick) {
929 return nick;
930 });
931 this.tabcomplete.data = ac_data;
932 }
933
934 if (inp_val[inp[0].selectionStart - 1] === ' ') {
935 return false;
936 }
937
938 (function () {
939 var tokens, // Words before the cursor position
940 val, // New value being built up
941 p1, // Position in the value just before the nick
942 newnick, // New nick to be displayed (cycles through)
943 range, // TextRange for setting new text cursor position
944 nick, // Current nick in the value
945 trailing = ': '; // Text to be inserted after a tabbed nick
946
947 tokens = inp_val.substring(0, inp[0].selectionStart).split(' ');
948 if (tokens[tokens.length-1] == ':')
949 tokens.pop();
950
951 nick = tokens[tokens.length - 1];
952
953 if (this.tabcomplete.prefix === '') {
954 this.tabcomplete.prefix = nick;
955 }
956
957 this.tabcomplete.data = _.select(this.tabcomplete.data, function (n) {
958 return (n.toLowerCase().indexOf(that.tabcomplete.prefix.toLowerCase()) === 0);
959 });
960
961 if (this.tabcomplete.data.length > 0) {
962 // Get the current value before cursor position
963 p1 = inp[0].selectionStart - (nick.length);
964 val = inp_val.substr(0, p1);
965
966 // Include the current selected nick
967 newnick = this.tabcomplete.data.shift();
968 this.tabcomplete.data.push(newnick);
969 val += newnick;
970
971 if (inp_val.substr(inp[0].selectionStart, 2) !== trailing)
972 val += trailing;
973
974 // Now include the rest of the current value
975 val += inp_val.substr(inp[0].selectionStart);
976
977 inp.val(val);
978
979 // Move the cursor position to the end of the nick
980 if (inp[0].setSelectionRange) {
981 inp[0].setSelectionRange(p1 + newnick.length + trailing.length, p1 + newnick.length + trailing.length);
982 } else if (inp[0].createTextRange) { // not sure if this bit is actually needed....
983 range = inp[0].createTextRange();
984 range.collapse(true);
985 range.moveEnd('character', p1 + newnick.length + trailing.length);
986 range.moveStart('character', p1 + newnick.length + trailing.length);
987 range.select();
988 }
989 }
990 }).apply(this);
991 return false;
992 }
993 },
994
995
996 processInput: function (command_raw) {
997 var command, params,
998 pre_processed;
999
1000 // The default command
1001 if (command_raw[0] !== '/' || command_raw.substr(0, 2) === '//') {
1002 // Remove any slash escaping at the start (ie. //)
1003 command_raw = command_raw.replace(/^\/\//, '/');
1004
1005 // Prepend the default command
1006 command_raw = '/msg ' + _kiwi.app.panels.active.get('name') + ' ' + command_raw;
1007 }
1008
1009 // Process the raw command for any aliases
1010 this.preprocessor.vars.server = _kiwi.gateway.get('name');
1011 this.preprocessor.vars.channel = _kiwi.app.panels.active.get('name');
1012 this.preprocessor.vars.destination = this.preprocessor.vars.channel;
1013 command_raw = this.preprocessor.process(command_raw);
1014
1015 // Extract the command and parameters
1016 params = command_raw.split(' ');
1017 if (params[0][0] === '/') {
1018 command = params[0].substr(1).toLowerCase();
1019 params = params.splice(1, params.length - 1);
1020 } else {
1021 // Default command
1022 command = 'msg';
1023 params.unshift(_kiwi.app.panels.active.get('name'));
1024 }
1025
1026 // Trigger the command events
1027 this.trigger('command', {command: command, params: params});
1028 this.trigger('command:' + command, {command: command, params: params});
1029
1030 // If we didn't have any listeners for this event, fire a special case
1031 // TODO: This feels dirty. Should this really be done..?
1032 if (!this._callbacks['command:' + command]) {
1033 this.trigger('unknown_command', {command: command, params: params});
1034 }
1035 },
1036
1037
1038 addPluginIcon: function ($icon) {
1039 var $tool = $('<div class="tool"></div>').append($icon);
1040 this.$el.find('.input_tools').append($tool);
1041 _kiwi.app.view.doLayout();
1042 }
1043 });
1044
1045
1046
1047
1048 _kiwi.view.StatusMessage = Backbone.View.extend({
1049 initialize: function () {
1050 this.$el.hide();
1051
1052 // Timer for hiding the message after X seconds
1053 this.tmr = null;
1054 },
1055
1056 text: function (text, opt) {
1057 // Defaults
1058 opt = opt || {};
1059 opt.type = opt.type || '';
1060 opt.timeout = opt.timeout || 5000;
1061
1062 this.$el.text(text).attr('class', opt.type);
1063 this.$el.slideDown($.proxy(_kiwi.app.view.doLayout, this));
1064
1065 if (opt.timeout) this.doTimeout(opt.timeout);
1066 },
1067
1068 html: function (html, opt) {
1069 // Defaults
1070 opt = opt || {};
1071 opt.type = opt.type || '';
1072 opt.timeout = opt.timeout || 5000;
1073
1074 this.$el.html(text).attr('class', opt.type);
1075 this.$el.slideDown(_kiwi.app.view.doLayout);
1076
1077 if (opt.timeout) this.doTimeout(opt.timeout);
1078 },
1079
1080 hide: function () {
1081 this.$el.slideUp($.proxy(_kiwi.app.view.doLayout, this));
1082 },
1083
1084 doTimeout: function (length) {
1085 if (this.tmr) clearTimeout(this.tmr);
1086 var that = this;
1087 this.tmr = setTimeout(function () { that.hide(); }, length);
1088 }
1089 });
1090
1091
1092
1093
1094 _kiwi.view.ResizeHandler = Backbone.View.extend({
1095 events: {
1096 'mousedown': 'startDrag',
1097 'mouseup': 'stopDrag'
1098 },
1099
1100 initialize: function () {
1101 this.dragging = false;
1102 this.starting_width = {};
1103
1104 $(window).on('mousemove', $.proxy(this.onDrag, this));
1105 },
1106
1107 startDrag: function (event) {
1108 this.dragging = true;
1109 },
1110
1111 stopDrag: function (event) {
1112 this.dragging = false;
1113 },
1114
1115 onDrag: function (event) {
1116 if (!this.dragging) return;
1117
1118 this.$el.css('left', event.clientX - (this.$el.outerWidth(true) / 2));
1119 $('#memberlists').css('width', this.$el.parent().width() - (this.$el.position().left + this.$el.outerWidth()));
1120 _kiwi.app.view.doLayout();
1121 }
1122 });
1123
1124
1125
1126 _kiwi.view.AppToolbar = Backbone.View.extend({
1127 events: {
1128 'click .settings': 'clickSettings'
1129 },
1130
1131 initialize: function () {
1132 },
1133
1134 clickSettings: function (event) {
1135 _kiwi.app.controlbox.processInput('/settings');
1136 }
1137 });
1138
1139
1140
1141 _kiwi.view.Application = Backbone.View.extend({
1142 initialize: function () {
1143 var that = this;
1144
1145 $(window).resize(function() { that.doLayout.apply(that); });
1146 $('#toolbar').resize(function() { that.doLayout.apply(that); });
1147 $('#controlbox').resize(function() { that.doLayout.apply(that); });
1148
1149 // Change the theme when the config is changed
1150 _kiwi.global.settings.on('change:theme', this.updateTheme, this);
1151 this.updateTheme(getQueryVariable('theme'));
1152
1153 _kiwi.global.settings.on('change:channel_list_style', this.setTabLayout, this);
1154 this.setTabLayout(_kiwi.global.settings.get('channel_list_style'));
1155
1156 _kiwi.global.settings.on('change:show_timestamps', this.displayTimestamps, this);
1157 this.displayTimestamps(_kiwi.global.settings.get('show_timestamps'));
1158
1159 this.doLayout();
1160
1161 $(document).keydown(this.setKeyFocus);
1162
1163 // Confirmation require to leave the page
1164 window.onbeforeunload = function () {
1165 if (_kiwi.gateway.isConnected()) {
1166 return 'This will close all KiwiIRC conversations. Are you sure you want to close this window?';
1167 }
1168 };
1169
1170 this.initSound();
1171 },
1172
1173
1174
1175 updateTheme: function (theme_name) {
1176 // If called by the settings callback, get the correct new_value
1177 if (theme_name === _kiwi.global.settings) {
1178 theme_name = arguments[1];
1179 }
1180
1181 // If we have no theme specified, get it from the settings
1182 if (!theme_name) theme_name = _kiwi.global.settings.get('theme');
1183
1184 // Clear any current theme
1185 this.$el.removeClass(function (i, css) {
1186 return (css.match(/\btheme_\S+/g) || []).join(' ');
1187 });
1188
1189 // Apply the new theme
1190 this.$el.addClass('theme_' + (theme_name || 'relaxed'));
1191 },
1192
1193
1194 setTabLayout: function (layout_style) {
1195 // If called by the settings callback, get the correct new_value
1196 if (layout_style === _kiwi.global.settings) {
1197 layout_style = arguments[1];
1198 }
1199
1200 if (layout_style == 'list') {
1201 this.$el.addClass('chanlist_treeview');
1202 } else {
1203 this.$el.removeClass('chanlist_treeview');
1204 }
1205
1206 this.doLayout();
1207 },
1208
1209
1210 displayTimestamps: function (show_timestamps) {
1211 // If called by the settings callback, get the correct new_value
1212 if (show_timestamps === _kiwi.global.settings) {
1213 show_timestamps = arguments[1];
1214 }
1215
1216 if (show_timestamps) {
1217 this.$el.addClass('timestamps');
1218 } else {
1219 this.$el.removeClass('timestamps');
1220 }
1221 },
1222
1223
1224 // Globally shift focus to the command input box on a keypress
1225 setKeyFocus: function (ev) {
1226 // If we're copying text, don't shift focus
1227 if (ev.ctrlKey || ev.altKey || ev.metaKey) {
1228 return;
1229 }
1230
1231 // If we're typing into an input box somewhere, ignore
1232 if ((ev.target.tagName.toLowerCase() === 'input') || (ev.target.tagName.toLowerCase() === 'textarea') || $(ev.target).attr('contenteditable')) {
1233 return;
1234 }
1235
1236 $('#controlbox .inp').focus();
1237 },
1238
1239
1240 doLayout: function () {
1241 var el_kiwi = this.$el;
1242 var el_panels = $('#kiwi #panels');
1243 var el_memberlists = $('#kiwi #memberlists');
1244 var el_toolbar = $('#kiwi #toolbar');
1245 var el_controlbox = $('#kiwi #controlbox');
1246 var el_resize_handle = $('#kiwi #memberlists_resize_handle');
1247
1248 var css_heights = {
1249 top: el_toolbar.outerHeight(true),
1250 bottom: el_controlbox.outerHeight(true)
1251 };
1252
1253
1254 // If any elements are not visible, full size the panals instead
1255 if (!el_toolbar.is(':visible')) {
1256 css_heights.top = 0;
1257 }
1258
1259 if (!el_controlbox.is(':visible')) {
1260 css_heights.bottom = 0;
1261 }
1262
1263 // Apply the CSS sizes
1264 el_panels.css(css_heights);
1265 el_memberlists.css(css_heights);
1266 el_resize_handle.css(css_heights);
1267
1268 // If we have channel tabs on the side, adjust the height
1269 if (el_kiwi.hasClass('chanlist_treeview')) {
1270 $('#tabs', el_kiwi).css(css_heights);
1271 }
1272
1273 // Determine if we have a narrow window (mobile/tablet/or even small desktop window)
1274 if (el_kiwi.outerWidth() < 400) {
1275 el_kiwi.addClass('narrow');
1276 } else {
1277 el_kiwi.removeClass('narrow');
1278 }
1279
1280 // Set the panels width depending on the memberlist visibility
1281 if (el_memberlists.css('display') != 'none') {
1282 // Panels to the side of the memberlist
1283 el_panels.css('right', el_memberlists.outerWidth(true));
1284 // The resize handle sits overlapping the panels and memberlist
1285 el_resize_handle.css('left', el_memberlists.position().left - (el_resize_handle.outerWidth(true) / 2));
1286 } else {
1287 // Memberlist is hidden so panels to the right edge
1288 el_panels.css('right', 0);
1289 // And move the handle just out of sight to the right
1290 el_resize_handle.css('left', el_panels.outerWidth(true));
1291 }
1292
1293 var input_wrap_width = parseInt($('#kiwi #controlbox .input_tools').outerWidth());
1294 el_controlbox.find('.input_wrap').css('right', input_wrap_width + 7);
1295 },
1296
1297
1298 alertWindow: function (title) {
1299 if (!this.alertWindowTimer) {
1300 this.alertWindowTimer = new (function () {
1301 var that = this;
1302 var tmr;
1303 var has_focus = true;
1304 var state = 0;
1305 var default_title = 'Kiwi IRC';
1306 var title = 'Kiwi IRC';
1307
1308 this.setTitle = function (new_title) {
1309 new_title = new_title || default_title;
1310 window.document.title = new_title;
1311 return new_title;
1312 };
1313
1314 this.start = function (new_title) {
1315 // Don't alert if we already have focus
1316 if (has_focus) return;
1317
1318 title = new_title;
1319 if (tmr) return;
1320 tmr = setInterval(this.update, 1000);
1321 };
1322
1323 this.stop = function () {
1324 // Stop the timer and clear the title
1325 if (tmr) clearInterval(tmr);
1326 tmr = null;
1327 this.setTitle();
1328
1329 // Some browsers don't always update the last title correctly
1330 // Wait a few seconds and then reset
1331 setTimeout(this.reset, 2000);
1332 };
1333
1334 this.reset = function () {
1335 if (tmr) return;
1336 that.setTitle();
1337 };
1338
1339
1340 this.update = function () {
1341 if (state === 0) {
1342 that.setTitle(title);
1343 state = 1;
1344 } else {
1345 that.setTitle();
1346 state = 0;
1347 }
1348 };
1349
1350 $(window).focus(function (event) {
1351 has_focus = true;
1352 that.stop();
1353
1354 // Some browsers don't always update the last title correctly
1355 // Wait a few seconds and then reset
1356 setTimeout(that.reset, 2000);
1357 });
1358
1359 $(window).blur(function (event) {
1360 has_focus = false;
1361 });
1362 })();
1363 }
1364
1365 this.alertWindowTimer.start(title);
1366 },
1367
1368
1369 barsHide: function (instant) {
1370 var that = this;
1371
1372 if (!instant) {
1373 $('#toolbar').slideUp({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
1374 $('#controlbox').slideUp({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
1375 } else {
1376 $('#toolbar').slideUp(0);
1377 $('#controlbox').slideUp(0);
1378 this.doLayout();
1379 }
1380 },
1381
1382 barsShow: function (instant) {
1383 var that = this;
1384
1385 if (!instant) {
1386 $('#toolbar').slideDown({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
1387 $('#controlbox').slideDown({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
1388 } else {
1389 $('#toolbar').slideDown(0);
1390 $('#controlbox').slideDown(0);
1391 this.doLayout();
1392 }
1393 },
1394
1395
1396 initSound: function () {
1397 var that = this,
1398 base_path = this.model.get('base_path');
1399
1400 $script(base_path + '/assets/libs/soundmanager2/soundmanager2-nodebug-jsmin.js', function() {
1401 if (typeof soundManager === 'undefined')
1402 return;
1403
1404 soundManager.setup({
1405 url: base_path + '/assets/libs/soundmanager2/',
1406 flashVersion: 9, // optional: shiny features (default = 8)// optional: ignore Flash where possible, use 100% HTML5 mode
1407 preferFlash: true,
1408
1409 onready: function() {
1410 that.sound_object = soundManager.createSound({
1411 id: 'highlight',
1412 url: base_path + '/assets/sound/highlight.mp3'
1413 });
1414 }
1415 });
1416 });
1417 },
1418
1419
1420 playSound: function (sound_id) {
1421 if (!this.sound_object) return;
1422
1423 if (_kiwi.global.settings.get('mute_sounds'))
1424 return;
1425
1426 soundManager.play(sound_id);
1427 }
1428 });
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438 _kiwi.view.MediaMessage = Backbone.View.extend({
1439 events: {
1440 'click .media_close': 'close'
1441 },
1442
1443 initialize: function () {
1444 // Get the URL from the data
1445 this.url = this.$el.data('url');
1446 },
1447
1448 // Close the media content and remove it from display
1449 close: function () {
1450 var that = this;
1451 this.$content.slideUp('fast', function () {
1452 that.$content.remove();
1453 });
1454 },
1455
1456 // Open the media content within its wrapper
1457 open: function () {
1458 // Create the content div if we haven't already
1459 if (!this.$content) {
1460 this.$content = $('<div class="media_content"><a class="media_close"><i class="icon-chevron-up"></i> Close media</a><br /><div class="content"></div></div>');
1461 this.$content.find('.content').append(this.mediaTypes[this.$el.data('type')].apply(this, []) || 'Not found :(');
1462 }
1463
1464 // Now show the content if not already
1465 if (!this.$content.is(':visible')) {
1466 // Hide it first so the slideDown always plays
1467 this.$content.hide();
1468
1469 // Add the media content and slide it into view
1470 this.$el.append(this.$content);
1471 this.$content.slideDown();
1472 }
1473 },
1474
1475
1476
1477 // Generate the media content for each recognised type
1478 mediaTypes: {
1479 twitter: function () {
1480 var tweet_id = this.$el.data('tweetid');
1481 var that = this;
1482
1483 $.getJSON('https://api.twitter.com/1/statuses/oembed.json?id=' + tweet_id + '&callback=?', function (data) {
1484 that.$content.find('.content').html(data.html);
1485 });
1486
1487 return $('<div>Loading tweet..</div>');
1488 },
1489
1490
1491 image: function () {
1492 return $('<a href="' + this.url + '" target="_blank"><img height="100" src="' + this.url + '" /></a>');
1493 },
1494
1495
1496 reddit: function () {
1497 var that = this;
1498 var matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(this.url);
1499
1500 $.getJSON('http://www.' + matches[0] + '.json?jsonp=?', function (data) {
1501 console.log('Loaded reddit data', data);
1502 var post = data[0].data.children[0].data;
1503 var thumb = '';
1504
1505 // Show a thumbnail if there is one
1506 if (post.thumbnail) {
1507 //post.thumbnail = 'http://www.eurotunnel.com/uploadedImages/commercial/back-steps-icon-arrow.png';
1508
1509 // Hide the thumbnail if an over_18 image
1510 if (post.over_18) {
1511 thumb = '<span class="thumbnail_nsfw" onclick="$(this).find(\'p\').remove(); $(this).find(\'img\').css(\'visibility\', \'visible\');">';
1512 thumb += '<p style="font-size:0.9em;line-height:1.2em;cursor:pointer;">Show<br />NSFW</p>';
1513 thumb += '<img src="' + post.thumbnail + '" class="thumbnail" style="visibility:hidden;" />';
1514 thumb += '</span>';
1515 } else {
1516 thumb = '<img src="' + post.thumbnail + '" class="thumbnail" />';
1517 }
1518 }
1519
1520 // Build the template string up
1521 var tmpl = '<div>' + thumb + '<b><%- title %></b><br />Posted by <%- author %>. &nbsp;&nbsp; ';
1522 tmpl += '<i class="icon-arrow-up"></i> <%- ups %> &nbsp;&nbsp; <i class="icon-arrow-down"></i> <%- downs %><br />';
1523 tmpl += '<%- num_comments %> comments made. <a href="http://www.reddit.com<%- permalink %>">View post</a></div>';
1524
1525 that.$content.find('.content').html(_.template(tmpl, post));
1526 });
1527
1528 return $('<div>Loading Reddit thread..</div>');
1529 }
1530 }
1531
1532 }, {
1533
1534 // Build the closed media HTML from a URL
1535 buildHtml: function (url) {
1536 var html = '', matches;
1537
1538 // Is it an image?
1539 if (url.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)) {
1540 html += '<span class="media image" data-type="image" data-url="' + url + '" title="Open Image"><a class="open"><i class="icon-chevron-right"></i></a></span>';
1541 }
1542
1543 // Is it a tweet?
1544 matches = (/https?:\/\/twitter.com\/([a-zA-Z0-9_]+)\/status\/([0-9]+)/ig).exec(url);
1545 if (matches) {
1546 html += '<span class="media twitter" data-type="twitter" data-url="' + url + '" data-tweetid="' + matches[2] + '" title="Show tweet information"><a class="open"><i class="icon-chevron-right"></i></a></span>';
1547 }
1548
1549 // Is reddit?
1550 matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(url);
1551 if (matches) {
1552 html += '<span class="media reddit" data-type="reddit" data-url="' + url + '" title="Reddit thread"><a class="open"><i class="icon-chevron-right"></i></a></span>';
1553 }
1554
1555 return html;
1556 }
1557 });