Using CSS to hide repeated nicks
[KiwiIRC.git] / client / assets / src / views / panel.js
1 _kiwi.view.Panel = Backbone.View.extend({
2 tagName: "div",
3 className: "panel messages",
4
5 events: {
6 "click .chan": "chanClick",
7 'click .media .open': 'mediaClick',
8 'mouseenter .msg .nick': 'msgEnter',
9 'mouseleave .msg .nick': 'msgLeave'
10 },
11
12 initialize: function (options) {
13 this.initializePanel(options);
14 },
15
16 initializePanel: function (options) {
17 this.$el.css('display', 'none');
18 options = options || {};
19
20 // Containing element for this panel
21 if (options.container) {
22 this.$container = $(options.container);
23 } else {
24 this.$container = $('#kiwi .panels .container1');
25 }
26
27 this.$el.appendTo(this.$container);
28
29 this.alert_level = 0;
30
31 this.model.bind('msg', this.newMsg, this);
32 this.msg_count = 0;
33
34 this.model.set({"view": this}, {"silent": true});
35 },
36
37 render: function () {
38 var that = this;
39
40 this.$el.empty();
41 _.each(this.model.get('scrollback'), function (msg) {
42 that.newMsg(msg);
43 });
44 },
45
46 newMsg: function (msg) {
47 var re, line_msg, $this = this.$el,
48 nick_colour_hex, nick_hex, is_highlight, msg_css_classes = '',
49 template_vars, time_difference,
50 sb = this.model.get('scrollback'),
51 prev_msg = sb[sb.length-2];
52
53 // Nick highlight detecting
54 if ((new RegExp('(^|\\W)(' + escapeRegex(_kiwi.app.connections.active_connection.get('nick')) + ')(\\W|$)', 'i')).test(msg.msg)) {
55 is_highlight = true;
56 msg_css_classes += ' highlight';
57 }
58
59 // Escape any HTML that may be in here
60 msg.msg = $('<div />').text(msg.msg).html();
61
62 // Make the channels clickable
63 re = new RegExp('(?:^|\\s)([' + escapeRegex(_kiwi.gateway.get('channel_prefix')) + '][^ ,.\\007]+)', 'g');
64 msg.msg = msg.msg.replace(re, function (match) {
65 return '<a class="chan" data-channel="' + match.trim() + '">' + match + '</a>';
66 });
67
68
69 // Parse any links found
70 msg.msg = msg.msg.replace(/(([A-Za-z][A-Za-z0-9\-]*\:\/\/)|(www\.))([\w.\-]+)([a-zA-Z]{2,6})(:[0-9]+)?(\/[\w#!:.?$'()[\]*,;~+=&%@!\-\/]*)?/gi, function (url) {
71 var nice = url,
72 extra_html = '';
73
74 // Add the http if no protoocol was found
75 if (url.match(/^www\./)) {
76 url = 'http://' + url;
77 }
78
79 // Shorten the displayed URL if it's going to be too long
80 if (nice.length > 100) {
81 nice = nice.substr(0, 100) + '...';
82 }
83
84 // Get any media HTML if supported
85 extra_html = _kiwi.view.MediaMessage.buildHtml(url);
86
87 // Make the link clickable
88 return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url + '">' + nice + '</a>' + extra_html;
89 });
90
91
92 // Convert IRC formatting into HTML formatting
93 msg.msg = formatIRCMsg(msg.msg);
94
95
96 // Add some colours to the nick (Method based on IRSSIs nickcolor.pl)
97 nick_colour_hex = (function (nick) {
98 var nick_int = 0, rgb;
99
100 _.map(nick.split(''), function (i) { nick_int += i.charCodeAt(0); });
101 rgb = hsl2rgb(nick_int % 255, 70, 35);
102 rgb = rgb[2] | (rgb[1] << 8) | (rgb[0] << 16);
103
104 return '#' + rgb.toString(16);
105 })(msg.nick);
106
107 msg.nick_style = 'color:' + nick_colour_hex + ';';
108
109 // Generate a hex string from the nick to be used as a CSS class name
110 nick_hex = msg.nick_css_class = '';
111 if (msg.nick) {
112 _.map(msg.nick.split(''), function (char) {
113 nick_hex += char.charCodeAt(0).toString(16);
114 });
115 msg_css_classes += ' nick_' + nick_hex;
116 }
117
118 template_vars = _.clone(msg);
119
120 if (prev_msg) {
121 // Time difference between this message and the last (in minutes)
122 time_difference = (template_vars.date.getTime() - prev_msg.date.getTime())/1000/60;
123 if (prev_msg.nick === template_vars.nick && time_difference < 1) {
124 msg_css_classes += ' repeated_nick';
125 }
126 }
127
128 // Build up and add the line
129 template_vars.msg_css_classes = msg_css_classes;
130 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>';
131 $this.append(_.template(line_msg, template_vars));
132
133 // Activity/alerts based on the type of new message
134 if (msg.type.match(/^action /)) {
135 this.alert('action');
136
137 } else if (is_highlight) {
138 _kiwi.app.view.alertWindow('* People are talking!');
139 _kiwi.app.view.playSound('highlight');
140 this.alert('highlight');
141
142 } else {
143 // If this is the active panel, send an alert out
144 if (this.model.isActive()) {
145 _kiwi.app.view.alertWindow('* People are talking!');
146 }
147 this.alert('activity');
148 }
149
150 if (this.model.isQuery() && !this.model.isActive()) {
151 _kiwi.app.view.alertWindow('* People are talking!');
152 _kiwi.app.view.playSound('highlight');
153 }
154
155 // Update the activity counters
156 (function () {
157 // Only inrement the counters if we're not the active panel
158 if (this.model.isActive()) return;
159
160 var $act = this.model.tab.find('.activity');
161 $act.text((parseInt($act.text(), 10) || 0) + 1);
162 if ($act.text() === '0') {
163 $act.addClass('zero');
164 } else {
165 $act.removeClass('zero');
166 }
167 }).apply(this);
168
169 this.scrollToBottom();
170
171 // Make sure our DOM isn't getting too large (Acts as scrollback)
172 this.msg_count++;
173 if (this.msg_count > (parseInt(_kiwi.global.settings.get('scrollback'), 10) || 250)) {
174 $('.msg:first', this.$el).remove();
175 this.msg_count--;
176 }
177 },
178 chanClick: function (event) {
179 if (event.target) {
180 _kiwi.gateway.join(null, $(event.target).data('channel'));
181 } else {
182 // IE...
183 _kiwi.gateway.join(null, $(event.srcElement).data('channel'));
184 }
185 },
186
187 mediaClick: function (event) {
188 var $media = $(event.target).parents('.media');
189 var media_message;
190
191 if ($media.data('media')) {
192 media_message = $media.data('media');
193 } else {
194 media_message = new _kiwi.view.MediaMessage({el: $media[0]});
195
196 // Cache this MediaMessage instance for when it's opened again
197 $media.data('media', media_message);
198 }
199
200 media_message.open();
201 },
202
203 // Cursor hovers over a message
204 msgEnter: function (event) {
205 var nick_class;
206
207 // Find a valid class that this element has
208 _.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) {
209 if (css_class.match(/^nick_[a-z0-9]+/i)) {
210 nick_class = css_class;
211 }
212 });
213
214 // If no class was found..
215 if (!nick_class) return;
216
217 $('.'+nick_class).addClass('global_nick_highlight');
218 },
219
220 // Cursor leaves message
221 msgLeave: function (event) {
222 var nick_class;
223
224 // Find a valid class that this element has
225 _.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) {
226 if (css_class.match(/^nick_[a-z0-9]+/i)) {
227 nick_class = css_class;
228 }
229 });
230
231 // If no class was found..
232 if (!nick_class) return;
233
234 $('.'+nick_class).removeClass('global_nick_highlight');
235 },
236
237 show: function () {
238 var $this = this.$el;
239
240 // Hide all other panels and show this one
241 this.$container.children('.panel').css('display', 'none');
242 $this.css('display', 'block');
243
244 // Show this panels memberlist
245 var members = this.model.get("members");
246 if (members) {
247 $('#kiwi .memberlists').removeClass('disabled');
248 members.view.show();
249 } else {
250 // Memberlist not found for this panel, hide any active ones
251 $('#kiwi .memberlists').addClass('disabled').children().removeClass('active');
252 }
253
254 // Remove any alerts and activity counters for this panel
255 this.alert('none');
256 this.model.tab.find('.activity').text('0').addClass('zero');
257
258 _kiwi.app.panels.trigger('active', this.model, _kiwi.app.panels().active);
259 this.model.trigger('active', this.model);
260
261 _kiwi.app.view.doLayout();
262
263 this.scrollToBottom(true);
264 },
265
266
267 alert: function (level) {
268 // No need to highlight if this si the active panel
269 if (this.model == _kiwi.app.panels().active) return;
270
271 var types, type_idx;
272 types = ['none', 'action', 'activity', 'highlight'];
273
274 // Default alert level
275 level = level || 'none';
276
277 // If this alert level does not exist, assume clearing current level
278 type_idx = _.indexOf(types, level);
279 if (!type_idx) {
280 level = 'none';
281 type_idx = 0;
282 }
283
284 // Only 'upgrade' the alert. Never down (unless clearing)
285 if (type_idx !== 0 && type_idx <= this.alert_level) {
286 return;
287 }
288
289 // Clear any existing levels
290 this.model.tab.removeClass(function (i, css) {
291 return (css.match(/\balert_\S+/g) || []).join(' ');
292 });
293
294 // Add the new level if there is one
295 if (level !== 'none') {
296 this.model.tab.addClass('alert_' + level);
297 }
298
299 this.alert_level = type_idx;
300 },
301
302
303 // Scroll to the bottom of the panel
304 scrollToBottom: function (force_down) {
305 // If this isn't the active panel, don't scroll
306 if (this.model !== _kiwi.app.panels().active) return;
307
308 // Don't scroll down if we're scrolled up the panel a little
309 if (force_down || this.$container.scrollTop() + this.$container.height() > this.$el.outerHeight() - 150) {
310 this.$container[0].scrollTop = this.$container[0].scrollHeight;
311 }
312 }
313 });