14630ab8df17f581899df63caa21286687a90d0e
[KiwiIRC.git] / client / src / views / channel.js
1 _kiwi.view.Channel = _kiwi.view.Panel.extend({
2 events: function(){
3 var parent_events = this.constructor.__super__.events;
4
5 if(_.isFunction(parent_events)){
6 parent_events = parent_events();
7 }
8 return _.extend({}, parent_events, {
9 'click .msg .nick' : 'nickClick',
10 'click .msg .inline-nick' : 'nickClick',
11 "click .chan": "chanClick",
12 'click .media .open': 'mediaClick',
13 'mouseenter .msg .nick': 'msgEnter',
14 'mouseleave .msg .nick': 'msgLeave'
15 });
16 },
17
18 initialize: function (options) {
19 this.initializePanel(options);
20
21 // Container for all the messages
22 this.$messages = $('<div class="messages"></div>');
23 this.$el.append(this.$messages);
24
25 this.model.bind('change:topic', this.topic, this);
26 this.model.bind('change:topic_set_by', this.topicSetBy, this);
27
28 if (this.model.get('members')) {
29 // When we join the memberlist, we have officially joined the channel
30 this.model.get('members').bind('add', function (member) {
31 if (member.get('nick') === this.model.collection.network.get('nick')) {
32 this.$el.find('.initial_loader').slideUp(function () {
33 $(this).remove();
34 });
35 }
36 }, this);
37
38 // Memberlist reset with a new nicklist? Consider we have joined
39 this.model.get('members').bind('reset', function(members) {
40 if (members.getByNick(this.model.collection.network.get('nick'))) {
41 this.$el.find('.initial_loader').slideUp(function () {
42 $(this).remove();
43 });
44 }
45 }, this);
46 }
47
48 // Only show the loader if this is a channel (ie. not a query)
49 if (this.model.isChannel()) {
50 this.$el.append('<div class="initial_loader" style="margin:1em;text-align:center;"> ' + _kiwi.global.i18n.translate('client_views_channel_joining').fetch() + ' <span class="loader"></span></div>');
51 }
52
53 this.model.bind('msg', this.newMsg, this);
54 this.msg_count = 0;
55 },
56
57
58 render: function () {
59 var that = this;
60
61 this.$messages.empty();
62 _.each(this.model.get('scrollback'), function (msg) {
63 that.newMsg(msg);
64 });
65 },
66
67
68 newMsg: function(msg) {
69
70 // Parse the msg object into properties fit for displaying
71 msg = this.generateMessageDisplayObj(msg);
72
73 _kiwi.global.events.emit('message:display', {panel: this.model, message: msg})
74 .then(_.bind(function() {
75 var line_msg;
76
77 // Format the nick to the config defined format
78 var display_obj = _.clone(msg);
79 display_obj.nick = styleText('message_nick', {nick: msg.nick, prefix: msg.nick_prefix || ''});
80
81 line_msg = '<div class="msg <%= type %> <%= css_classes %>"><div class="time"><%- time_string %></div><div class="nick" style="<%= nick_style %>"><%- nick %></div><div class="text" style="<%= style %>"><%= msg %> </div></div>';
82 this.$messages.append($(_.template(line_msg, display_obj)).data('message', msg));
83
84 this.updateLastSeenMarker();
85
86 // Activity/alerts based on the type of new message
87 if (msg.type.match(/^action /)) {
88 this.alert('action');
89
90 } else if (msg.is_highlight) {
91 _kiwi.app.view.alertWindow('* ' + _kiwi.global.i18n.translate('client_views_panel_activity').fetch());
92 _kiwi.app.view.favicon.newHighlight();
93 _kiwi.app.view.playSound('highlight');
94 _kiwi.app.view.showNotification(this.model.get('name'), msg.unparsed_msg);
95 this.alert('highlight');
96
97 } else {
98 // If this is the active panel, send an alert out
99 if (this.model.isActive()) {
100 _kiwi.app.view.alertWindow('* ' + _kiwi.global.i18n.translate('client_views_panel_activity').fetch());
101 }
102 this.alert('activity');
103 }
104
105 if (this.model.isQuery() && !this.model.isActive()) {
106 _kiwi.app.view.alertWindow('* ' + _kiwi.global.i18n.translate('client_views_panel_activity').fetch());
107
108 // Highlights have already been dealt with above
109 if (!msg.is_highlight) {
110 _kiwi.app.view.favicon.newHighlight();
111 }
112
113 _kiwi.app.view.showNotification(this.model.get('name'), msg.unparsed_msg);
114 _kiwi.app.view.playSound('highlight');
115 }
116
117 // Update the activity counters
118 (function () {
119 // Only inrement the counters if we're not the active panel
120 if (this.model.isActive()) return;
121
122 var count_all_activity = _kiwi.global.settings.get('count_all_activity'),
123 exclude_message_types, new_count;
124
125 // Set the default config value
126 if (typeof count_all_activity === 'undefined') {
127 count_all_activity = false;
128 }
129
130 // Do not increment the counter for these message types
131 exclude_message_types = [
132 'action join',
133 'action quit',
134 'action part',
135 'action kick',
136 'action nick',
137 'action mode'
138 ];
139
140 if (count_all_activity || _.indexOf(exclude_message_types, msg.type) === -1) {
141 new_count = this.model.get('activity_counter') || 0;
142 new_count++;
143 this.model.set('activity_counter', new_count);
144 }
145
146 }).apply(this);
147
148 if(this.model.isActive()) this.scrollToBottom();
149
150 // Make sure our DOM isn't getting too large (Acts as scrollback)
151 this.msg_count++;
152 if (this.msg_count > (parseInt(_kiwi.global.settings.get('scrollback'), 10) || 250)) {
153 $('.msg:first', this.$messages).remove();
154 this.msg_count--;
155 }
156 }, this));
157 },
158
159
160 // Let nicks be clickable + colourise within messages
161 parseMessageNicks: function(word, colourise) {
162 var members, member, colour = '';
163
164 members = this.model.get('members');
165 if (!members) {
166 return;
167 }
168
169 member = members.getByNick(word);
170 if (!member) {
171 return;
172 }
173
174 if (colourise !== false) {
175 // Use the nick from the member object so the colour matches the letter casing
176 colour = this.getNickColour(member.get('nick'));
177 colour = 'color:' + colour;
178 }
179
180 return _.template('<span class="inline-nick" style="<%- colour %>;cursor:pointer;" data-nick="<%- nick %>"><%- nick %></span>', {
181 nick: word,
182 colour: colour
183 });
184
185 },
186
187
188 // Make channels clickable
189 parseMessageChannels: function(word) {
190 var re,
191 parsed = false,
192 network = this.model.get('network');
193
194 if (!network) {
195 return;
196 }
197
198 re = new RegExp('(^|\\s)([' + escapeRegex(network.get('channel_prefix')) + '][^ ,\\007]+)', 'g');
199
200 if (!word.match(re)) {
201 return parsed;
202 }
203
204 parsed = word.replace(re, function (m1, m2) {
205 return m2 + '<a class="chan" data-channel="' + _.escape(m1.trim()) + '">' + _.escape(m1.trim()) + '</a>';
206 });
207
208 return parsed;
209 },
210
211
212 parseMessageUrls: function(word) {
213 var found_a_url = false,
214 parsed_url;
215
216 parsed_url = word.replace(/^(([A-Za-z][A-Za-z0-9\-]*\:\/\/)|(www\.))([\w.\-]+)([a-zA-Z]{2,6})(:[0-9]+)?(\/[\w!:.?$'()[\]*,;~+=&%@!\-\/]*)?(#.*)?$/gi, function (url) {
217 var nice = url,
218 extra_html = '';
219
220 // Don't allow javascript execution
221 if (url.match(/^javascript:/)) {
222 return url;
223 }
224
225 found_a_url = true;
226
227 // Add the http if no protoocol was found
228 if (url.match(/^www\./)) {
229 url = 'http://' + url;
230 }
231
232 // Shorten the displayed URL if it's going to be too long
233 if (nice.length > 100) {
234 nice = nice.substr(0, 100) + '...';
235 }
236
237 // Get any media HTML if supported
238 extra_html = _kiwi.view.MediaMessage.buildHtml(url);
239
240 // Make the link clickable
241 return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url.replace(/"/g, '%22') + '">' + _.escape(nice) + '</a>' + extra_html;
242 });
243
244 return found_a_url ? parsed_url : false;
245 },
246
247
248 // Get a colour from a nick (Method based on IRSSIs nickcolor.pl)
249 getNickColour: function(nick) {
250 var nick_int = 0, rgb;
251
252 _.map(nick.split(''), function (i) { nick_int += i.charCodeAt(0); });
253 rgb = hsl2rgb(nick_int % 255, 70, 35);
254 rgb = rgb[2] | (rgb[1] << 8) | (rgb[0] << 16);
255
256 return '#' + rgb.toString(16);
257 },
258
259
260 // Takes an IRC message object and parses it for displaying
261 generateMessageDisplayObj: function(msg) {
262 var nick_hex, time_difference,
263 message_words,
264 sb = this.model.get('scrollback'),
265 prev_msg = sb[sb.length-2],
266 hour, pm, am_pm_locale_key;
267
268 // Clone the msg object so we dont modify the original
269 msg = _.clone(msg);
270
271 // Defaults
272 msg.css_classes = '';
273 msg.nick_style = '';
274 msg.is_highlight = false;
275 msg.time_string = '';
276
277
278 // Nick highlight detecting
279 var nick = _kiwi.app.connections.active_connection.get('nick');
280 if ((new RegExp('(^|\\W)(' + escapeRegex(nick) + ')(\\W|$)', 'i')).test(msg.msg)) {
281 // Do not highlight the user's own input
282 if (msg.nick.localeCompare(nick) !== 0) {
283 msg.is_highlight = true;
284 msg.css_classes += ' highlight';
285 }
286 }
287
288 message_words = msg.msg.split(' ');
289 message_words = _.map(message_words, function(word) {
290 var parsed_word;
291
292 parsed_word = this.parseMessageUrls(word);
293 if (typeof parsed_word === 'string') return parsed_word;
294
295 parsed_word = this.parseMessageChannels(word);
296 if (typeof parsed_word === 'string') return parsed_word;
297
298 parsed_word = this.parseMessageNicks(word, (msg.type === 'privmsg'));
299 if (typeof parsed_word === 'string') return parsed_word;
300
301 parsed_word = _.escape(word);
302
303 // Replace text emoticons with images
304 if (_kiwi.global.settings.get('show_emoticons')) {
305 parsed_word = emoticonFromText(parsed_word);
306 }
307
308 return parsed_word;
309 }, this);
310
311 msg.unparsed_msg = msg.msg;
312 msg.msg = message_words.join(' ');
313
314 // Convert IRC formatting into HTML formatting
315 msg.msg = formatIRCMsg(msg.msg);
316
317 // Add some colours to the nick
318 msg.nick_style = 'color:' + this.getNickColour(msg.nick) + ';';
319
320 // Generate a hex string from the nick to be used as a CSS class name
321 nick_hex = '';
322 if (msg.nick) {
323 _.map(msg.nick.split(''), function (char) {
324 nick_hex += char.charCodeAt(0).toString(16);
325 });
326 msg.css_classes += ' nick_' + nick_hex;
327 }
328
329 if (prev_msg) {
330 // Time difference between this message and the last (in minutes)
331 time_difference = (msg.time.getTime() - prev_msg.time.getTime())/1000/60;
332 if (prev_msg.nick === msg.nick && time_difference < 1) {
333 msg.css_classes += ' repeated_nick';
334 }
335 }
336
337 // Build up and add the line
338 if (_kiwi.global.settings.get('use_24_hour_timestamps')) {
339 msg.time_string = msg.time.getHours().toString().lpad(2, "0") + ":" + msg.time.getMinutes().toString().lpad(2, "0") + ":" + msg.time.getSeconds().toString().lpad(2, "0");
340 } else {
341 hour = msg.time.getHours();
342 pm = hour > 11;
343
344 hour = hour % 12;
345 if (hour === 0)
346 hour = 12;
347
348 am_pm_locale_key = pm ?
349 'client_views_panel_timestamp_pm' :
350 'client_views_panel_timestamp_am';
351
352 msg.time_string = translateText(am_pm_locale_key, hour + ":" + msg.time.getMinutes().toString().lpad(2, "0") + ":" + msg.time.getSeconds().toString().lpad(2, "0"));
353 }
354
355 return msg;
356 },
357
358
359 topic: function (topic) {
360 if (typeof topic !== 'string' || !topic) {
361 topic = this.model.get("topic");
362 }
363
364 this.model.addMsg('', styleText('channel_topic', {text: topic, channel: this.model.get('name')}), 'topic');
365
366 // If this is the active channel then update the topic bar
367 if (_kiwi.app.panels().active === this.model) {
368 _kiwi.app.topicbar.setCurrentTopicFromChannel(this.model);
369 }
370 },
371
372 topicSetBy: function (topic) {
373 // If this is the active channel then update the topic bar
374 if (_kiwi.app.panels().active === this.model) {
375 _kiwi.app.topicbar.setCurrentTopicFromChannel(this.model);
376 }
377 },
378
379 // Click on a nickname
380 nickClick: function (event) {
381 var $target = $(event.currentTarget),
382 nick,
383 members = this.model.get('members'),
384 member;
385
386 event.stopPropagation();
387
388 // Check this current element for a nick before resorting to the main message
389 // (eg. inline nicks has the nick on its own element within the message)
390 nick = $target.data('nick');
391 if (!nick) {
392 nick = $target.parent('.msg').data('message').nick;
393 }
394
395 // Make sure this nick is still in the channel
396 member = members ? members.getByNick(nick) : null;
397 if (!member) {
398 return;
399 }
400
401 _kiwi.global.events.emit('nick:select', {target: $target, member: member, source: 'message'})
402 .then(_.bind(this.openUserMenuForNick, this, $target, member));
403 },
404
405
406 updateLastSeenMarker: function() {
407 var last_seen, last_message;
408 if (this.model.isActive() && _kiwi.app.view.has_focus) {
409 // Remove the previous last seen classes
410 last_seen = this.$(".last_seen");
411 if (last_seen && last_seen.length) {
412 last_seen.removeClass("last_seen");
413 }
414
415 // Mark the last message the user saw
416 last_message = this.$messages.children().last();
417 if (last_message) {
418 last_message.addClass("last_seen");
419 }
420 }
421 },
422
423
424 openUserMenuForNick: function ($target, member) {
425 var members = this.model.get('members'),
426 are_we_an_op = !!members.getByNick(_kiwi.app.connections.active_connection.get('nick')).get('is_op'),
427 userbox, menubox;
428
429 userbox = new _kiwi.view.UserBox();
430 userbox.setTargets(member, this.model);
431 userbox.displayOpItems(are_we_an_op);
432
433 menubox = new _kiwi.view.MenuBox(member.get('nick') || 'User');
434 menubox.addItem('userbox', userbox.$el);
435 menubox.showFooter(false);
436
437 _kiwi.global.events.emit('usermenu:created', {menu: menubox, userbox: userbox, user: member})
438 .then(_.bind(function() {
439 menubox.show();
440
441 // Position the userbox + menubox
442 var target_offset = $target.offset(),
443 t = target_offset.top,
444 m_bottom = t + menubox.$el.outerHeight(), // Where the bottom of menu will be
445 memberlist_bottom = this.$el.parent().offset().top + this.$el.parent().outerHeight();
446
447 // If the bottom of the userbox is going to be too low.. raise it
448 if (m_bottom > memberlist_bottom){
449 t = memberlist_bottom - menubox.$el.outerHeight();
450 }
451
452 // Set the new positon
453 menubox.$el.offset({
454 left: target_offset.left,
455 top: t
456 });
457 }, this))
458 .catch(_.bind(function() {
459 userbox = null;
460
461 menu.dispose();
462 menu = null;
463 }, this));
464 },
465
466
467 chanClick: function (event) {
468 var target = (event.target) ? $(event.target).data('channel') : $(event.srcElement).data('channel');
469
470 _kiwi.app.connections.active_connection.gateway.join(target);
471 },
472
473
474 mediaClick: function (event) {
475 var $media = $(event.target).parents('.media');
476 var media_message;
477
478 if ($media.data('media')) {
479 media_message = $media.data('media');
480 } else {
481 media_message = new _kiwi.view.MediaMessage({el: $media[0]});
482
483 // Cache this MediaMessage instance for when it's opened again
484 $media.data('media', media_message);
485 }
486
487 media_message.toggle();
488 },
489
490
491 // Cursor hovers over a message
492 msgEnter: function (event) {
493 var nick_class;
494
495 // Find a valid class that this element has
496 _.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) {
497 if (css_class.match(/^nick_[a-z0-9]+/i)) {
498 nick_class = css_class;
499 }
500 });
501
502 // If no class was found..
503 if (!nick_class) return;
504
505 $('.'+nick_class).addClass('global_nick_highlight');
506 },
507
508
509 // Cursor leaves message
510 msgLeave: 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).removeClass('global_nick_highlight');
524 }
525 });