1 _kiwi
.view
.Channel
= _kiwi
.view
.Panel
.extend({
3 var parent_events
= this.constructor.__super__
.events
;
5 if(_
.isFunction(parent_events
)){
6 parent_events
= parent_events();
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'
18 initialize: function (options
) {
19 this.initializePanel(options
);
21 // Container for all the messages
22 this.$messages
= $('<div class="messages"></div>');
23 this.$el
.append(this.$messages
);
25 this.model
.bind('change:topic', this.topic
, this);
26 this.model
.bind('change:topic_set_by', this.topicSetBy
, this);
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 () {
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 () {
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>');
53 this.model
.bind('msg', this.newMsg
, this);
61 this.$messages
.empty();
62 _
.each(this.model
.get('scrollback'), function (msg
) {
68 newMsg: function(msg
) {
70 // Parse the msg object into properties fit for displaying
71 msg
= this.generateMessageDisplayObj(msg
);
73 _kiwi
.global
.events
.emit('message:display', {panel
: this.model
, message
: msg
})
74 .then(_
.bind(function() {
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
|| ''});
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
));
84 // Activity/alerts based on the type of new message
85 if (msg
.type
.match(/^action /)) {
88 } else if (msg
.is_highlight
) {
89 _kiwi
.app
.view
.alertWindow('* ' + _kiwi
.global
.i18n
.translate('client_views_panel_activity').fetch());
90 _kiwi
.app
.view
.favicon
.newHighlight();
91 _kiwi
.app
.view
.playSound('highlight');
92 _kiwi
.app
.view
.showNotification(this.model
.get('name'), msg
.unparsed_msg
);
93 this.alert('highlight');
96 // If this is the active panel, send an alert out
97 if (this.model
.isActive()) {
98 _kiwi
.app
.view
.alertWindow('* ' + _kiwi
.global
.i18n
.translate('client_views_panel_activity').fetch());
100 this.alert('activity');
103 if (this.model
.isQuery() && !this.model
.isActive()) {
104 _kiwi
.app
.view
.alertWindow('* ' + _kiwi
.global
.i18n
.translate('client_views_panel_activity').fetch());
106 // Highlights have already been dealt with above
107 if (!msg
.is_highlight
) {
108 _kiwi
.app
.view
.favicon
.newHighlight();
111 _kiwi
.app
.view
.showNotification(this.model
.get('name'), msg
.unparsed_msg
);
112 _kiwi
.app
.view
.playSound('highlight');
115 // Update the activity counters
117 // Only inrement the counters if we're not the active panel
118 if (this.model
.isActive()) return;
120 var count_all_activity
= _kiwi
.global
.settings
.get('count_all_activity'),
121 exclude_message_types
, new_count
;
123 // Set the default config value
124 if (typeof count_all_activity
=== 'undefined') {
125 count_all_activity
= false;
128 // Do not increment the counter for these message types
129 exclude_message_types
= [
138 if (count_all_activity
|| _
.indexOf(exclude_message_types
, msg
.type
) === -1) {
139 new_count
= this.model
.get('activity_counter') || 0;
141 this.model
.set('activity_counter', new_count
);
146 if(this.model
.isActive()) this.scrollToBottom();
148 // Make sure our DOM isn't getting too large (Acts as scrollback)
150 if (this.msg_count
> (parseInt(_kiwi
.global
.settings
.get('scrollback'), 10) || 250)) {
151 $('.msg:first', this.$messages
).remove();
158 // Let nicks be clickable + colourise within messages
159 parseMessageNicks: function(word
, colourise
) {
160 var members
, member
, style
= '';
162 members
= this.model
.get('members');
167 member
= members
.getByNick(word
);
172 if (colourise
!== false) {
173 // Use the nick from the member object so the style matches the letter casing
174 style
= this.getNickStyles(member
.get('nick')).asCssString();
177 return _
.template('<span class="inline-nick" style="<%- style %>;cursor:pointer;" data-nick="<%- nick %>"><%- nick %></span>', {
185 // Make channels clickable
186 parseMessageChannels: function(word
) {
189 network
= this.model
.get('network');
195 re
= new RegExp('(^|\\s)([' + escapeRegex(network
.get('channel_prefix')) + '][^ ,\\007]+)', 'g');
197 if (!word
.match(re
)) {
201 parsed
= word
.replace(re
, function (m1
, m2
) {
202 return m2
+ '<a class="chan" data-channel="' + _
.escape(m1
.trim()) + '">' + _
.escape(m1
.trim()) + '</a>';
209 parseMessageUrls: function(word
) {
210 var found_a_url
= false,
213 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 // Don't allow javascript execution
218 if (url
.match(/^javascript:/)) {
224 // Add the http if no protoocol was found
225 if (url
.match(/^www\./)) {
226 url
= 'http://' + url
;
229 // Shorten the displayed URL if it's going to be too long
230 if (nice
.length
> 100) {
231 nice
= nice
.substr(0, 100) + '...';
234 // Get any media HTML if supported
235 extra_html
= _kiwi
.view
.MediaMessage
.buildHtml(url
);
237 // Make the link clickable
238 return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url
.replace(/"/g, '%22') + '">' + _.escape(nice) + '</a>' + extra_html;
241 return found_a_url ? parsed_url : false;
245 // Sgnerate a css style for a nick
246 getNickStyles: function(nick) {
247 var ret, colour, nick_int = 0, rgb, nick_lightness;
249 // Get a colour from a nick (Method based on IRSSIs nickcolor.pl)
250 _.map(nick.split(''), function (i) { nick_int += i.charCodeAt(0); });
252 nick_lightness = (_.find(_kiwi.app.themes, function (theme) {
253 return theme.name.toLowerCase() === _kiwi.global.settings.get('theme
').toLowerCase();
254 }) || {}).nick_lightness;
256 if (typeof nick_lightness !== 'number
') {
259 nick_lightness = Math.max(0, Math.min(100, nick_lightness));
262 rgb = hsl2rgb(nick_int % 255, 70, nick_lightness);
263 rgb = rgb[2] | (rgb[1] << 8) | (rgb[0] << 16);
264 colour = '#' + rgb.toString(16);
266 ret = {color: colour};
267 ret.asCssString = function() {
268 return _.reduce(this, function(result, item, key){
269 return result + key + ':' + item + ';';
277 // Takes an IRC message object and parses it for displaying
278 generateMessageDisplayObj: function(msg) {
279 var nick_hex, time_difference,
281 sb = this.model.get('scrollback
'),
282 prev_msg = sb[sb.length-2],
283 hour, pm, am_pm_locale_key;
285 // Clone the msg object so we dont modify the original
289 msg.css_classes = '';
291 msg.is_highlight = false;
292 msg.time_string = '';
295 // Nick highlight detecting
296 var nick = _kiwi.app.connections.active_connection.get('nick
');
297 if ((new RegExp('(^|\\W
)(' + escapeRegex(nick) + ')(\\W
|$)', 'i
')).test(msg.msg)) {
298 // Do not highlight the user's own input
299 if (msg
.nick
.localeCompare(nick
) !== 0) {
300 msg
.is_highlight
= true;
301 msg
.css_classes
+= ' highlight';
305 message_words
= msg
.msg
.split(' ');
306 message_words
= _
.map(message_words
, function(word
) {
309 parsed_word
= this.parseMessageUrls(word
);
310 if (typeof parsed_word
=== 'string') return parsed_word
;
312 parsed_word
= this.parseMessageChannels(word
);
313 if (typeof parsed_word
=== 'string') return parsed_word
;
315 parsed_word
= this.parseMessageNicks(word
, (msg
.type
=== 'privmsg'));
316 if (typeof parsed_word
=== 'string') return parsed_word
;
318 parsed_word
= _
.escape(word
);
320 // Replace text emoticons with images
321 if (_kiwi
.global
.settings
.get('show_emoticons')) {
322 parsed_word
= emoticonFromText(parsed_word
);
328 msg
.unparsed_msg
= msg
.msg
;
329 msg
.msg
= message_words
.join(' ');
331 // Convert IRC formatting into HTML formatting
332 msg
.msg
= formatIRCMsg(msg
.msg
);
334 // Add some style to the nick
335 msg
.nick_style
= this.getNickStyles(msg
.nick
).asCssString();
337 // Generate a hex string from the nick to be used as a CSS class name
340 _
.map(msg
.nick
.split(''), function (char) {
341 nick_hex
+= char.charCodeAt(0).toString(16);
343 msg
.css_classes
+= ' nick_' + nick_hex
;
347 // Time difference between this message and the last (in minutes)
348 time_difference
= (msg
.time
.getTime() - prev_msg
.time
.getTime())/1000/60;
349 if (prev_msg
.nick
=== msg
.nick
&& time_difference
< 1) {
350 msg
.css_classes
+= ' repeated_nick';
354 // Build up and add the line
355 if (_kiwi
.global
.settings
.get('use_24_hour_timestamps')) {
356 msg
.time_string
= msg
.time
.getHours().toString().lpad(2, "0") + ":" + msg
.time
.getMinutes().toString().lpad(2, "0") + ":" + msg
.time
.getSeconds().toString().lpad(2, "0");
358 hour
= msg
.time
.getHours();
365 am_pm_locale_key
= pm
?
366 'client_views_panel_timestamp_pm' :
367 'client_views_panel_timestamp_am';
369 msg
.time_string
= translateText(am_pm_locale_key
, hour
+ ":" + msg
.time
.getMinutes().toString().lpad(2, "0") + ":" + msg
.time
.getSeconds().toString().lpad(2, "0"));
376 topic: function (topic
) {
377 if (typeof topic
!== 'string' || !topic
) {
378 topic
= this.model
.get("topic");
381 this.model
.addMsg('', styleText('channel_topic', {text
: topic
, channel
: this.model
.get('name')}), 'topic');
383 // If this is the active channel then update the topic bar
384 if (_kiwi
.app
.panels().active
=== this.model
) {
385 _kiwi
.app
.topicbar
.setCurrentTopicFromChannel(this.model
);
389 topicSetBy: function (topic
) {
390 // If this is the active channel then update the topic bar
391 if (_kiwi
.app
.panels().active
=== this.model
) {
392 _kiwi
.app
.topicbar
.setCurrentTopicFromChannel(this.model
);
396 // Click on a nickname
397 nickClick: function (event
) {
398 var $target
= $(event
.currentTarget
),
400 members
= this.model
.get('members'),
403 event
.stopPropagation();
405 // Check this current element for a nick before resorting to the main message
406 // (eg. inline nicks has the nick on its own element within the message)
407 nick
= $target
.data('nick');
409 nick
= $target
.parent('.msg').data('message').nick
;
412 // Make sure this nick is still in the channel
413 member
= members
? members
.getByNick(nick
) : null;
418 _kiwi
.global
.events
.emit('nick:select', {target
: $target
, member
: member
, source
: 'message'})
419 .then(_
.bind(this.openUserMenuForNick
, this, $target
, member
));
423 updateLastSeenMarker: function() {
424 if (this.model
.isActive()) {
425 // Remove the previous last seen classes
426 this.$(".last_seen").removeClass("last_seen");
428 // Mark the last message the user saw
429 this.$messages
.children().last().addClass("last_seen");
434 openUserMenuForNick: function ($target
, member
) {
435 var members
= this.model
.get('members'),
436 are_we_an_op
= !!members
.getByNick(_kiwi
.app
.connections
.active_connection
.get('nick')).get('is_op'),
439 userbox
= new _kiwi
.view
.UserBox();
440 userbox
.setTargets(member
, this.model
);
441 userbox
.displayOpItems(are_we_an_op
);
443 menubox
= new _kiwi
.view
.MenuBox(member
.get('nick') || 'User');
444 menubox
.addItem('userbox', userbox
.$el
);
445 menubox
.showFooter(false);
447 _kiwi
.global
.events
.emit('usermenu:created', {menu
: menubox
, userbox
: userbox
, user
: member
})
448 .then(_
.bind(function() {
451 // Position the userbox + menubox
452 var target_offset
= $target
.offset(),
453 t
= target_offset
.top
,
454 m_bottom
= t
+ menubox
.$el
.outerHeight(), // Where the bottom of menu will be
455 memberlist_bottom
= this.$el
.parent().offset().top
+ this.$el
.parent().outerHeight();
457 // If the bottom of the userbox is going to be too low.. raise it
458 if (m_bottom
> memberlist_bottom
){
459 t
= memberlist_bottom
- menubox
.$el
.outerHeight();
462 // Set the new positon
464 left
: target_offset
.left
,
468 .catch(_
.bind(function() {
477 chanClick: function (event
) {
478 var target
= (event
.target
) ? $(event
.target
).data('channel') : $(event
.srcElement
).data('channel');
480 _kiwi
.app
.connections
.active_connection
.gateway
.join(target
);
484 mediaClick: function (event
) {
485 var $media
= $(event
.target
).parents('.media');
488 if ($media
.data('media')) {
489 media_message
= $media
.data('media');
491 media_message
= new _kiwi
.view
.MediaMessage({el
: $media
[0]});
493 // Cache this MediaMessage instance for when it's opened again
494 $media
.data('media', media_message
);
497 media_message
.toggle();
501 // Cursor hovers over a message
502 msgEnter: function (event
) {
505 // Find a valid class that this element has
506 _
.each($(event
.currentTarget
).parent('.msg').attr('class').split(' '), function (css_class
) {
507 if (css_class
.match(/^nick_[a-z0-9]+/i)) {
508 nick_class
= css_class
;
512 // If no class was found..
513 if (!nick_class
) return;
515 $('.'+nick_class
).addClass('global_nick_highlight');
519 // Cursor leaves message
520 msgLeave: function (event
) {
523 // Find a valid class that this element has
524 _
.each($(event
.currentTarget
).parent('.msg').attr('class').split(' '), function (css_class
) {
525 if (css_class
.match(/^nick_[a-z0-9]+/i)) {
526 nick_class
= css_class
;
530 // If no class was found..
531 if (!nick_class
) return;
533 $('.'+nick_class
).removeClass('global_nick_highlight');