Merge branch 'development'
[KiwiIRC.git] / client / src / views / channel.js
index 24f9397508ec1968d9cc71b5e4d1665606b98d8f..8a5573268211b695cfe3b9b5e71d54f574e91cc4 100644 (file)
@@ -7,6 +7,7 @@ _kiwi.view.Channel = _kiwi.view.Panel.extend({
         }
         return _.extend({}, parent_events, {
             'click .msg .nick' : 'nickClick',
+            'click .msg .inline-nick' : 'nickClick',
             "click .chan": "chanClick",
             'click .media .open': 'mediaClick',
             'mouseenter .msg .nick': 'msgEnter',
@@ -22,8 +23,10 @@ _kiwi.view.Channel = _kiwi.view.Panel.extend({
         this.$el.append(this.$messages);
 
         this.model.bind('change:topic', this.topic, this);
+        this.model.bind('change:topic_set_by', this.topicSetBy, this);
 
         if (this.model.get('members')) {
+            // When we join the memberlist, we have officially joined the channel
             this.model.get('members').bind('add', function (member) {
                 if (member.get('nick') === this.model.collection.network.get('nick')) {
                     this.$el.find('.initial_loader').slideUp(function () {
@@ -31,6 +34,15 @@ _kiwi.view.Channel = _kiwi.view.Panel.extend({
                     });
                 }
             }, this);
+
+            // Memberlist reset with a new nicklist? Consider we have joined
+            this.model.get('members').bind('reset', function(members) {
+                if (members.getByNick(this.model.collection.network.get('nick'))) {
+                    this.$el.find('.initial_loader').slideUp(function () {
+                        $(this).remove();
+                    });
+                }
+            }, this);
         }
 
         // Only show the loader if this is a channel (ie. not a query)
@@ -53,37 +65,162 @@ _kiwi.view.Channel = _kiwi.view.Panel.extend({
     },
 
 
-    newMsg: function (msg) {
-        var re, line_msg,
-            nick_colour_hex, nick_hex, is_highlight, msg_css_classes = '',
-            time_difference,
-            sb = this.model.get('scrollback'),
-            prev_msg = sb[sb.length-2],
-            network, hour, pm;
+    newMsg: function(msg) {
 
-        // Nick highlight detecting
-        if ((new RegExp('(^|\\W)(' + escapeRegex(_kiwi.app.connections.active_connection.get('nick')) + ')(\\W|$)', 'i')).test(msg.msg)) {
-            is_highlight = true;
-            msg_css_classes += ' highlight';
+        // Parse the msg object into properties fit for displaying
+        msg = this.generateMessageDisplayObj(msg);
+
+        _kiwi.global.events.emit('message:display', {panel: this.model, message: msg})
+        .then(_.bind(function() {
+            var line_msg;
+
+            // Format the nick to the config defined format
+            var display_obj = _.clone(msg);
+            display_obj.nick = styleText('message_nick', {nick: msg.nick, prefix: msg.nick_prefix || ''});
+
+            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>';
+            this.$messages.append($(_.template(line_msg, display_obj)).data('message', msg));
+
+            // Activity/alerts based on the type of new message
+            if (msg.type.match(/^action /)) {
+                this.alert('action');
+
+            } else if (msg.is_highlight) {
+                _kiwi.app.view.alertWindow('* ' + _kiwi.global.i18n.translate('client_views_panel_activity').fetch());
+                _kiwi.app.view.favicon.newHighlight();
+                _kiwi.app.view.playSound('highlight');
+                _kiwi.app.view.showNotification(this.model.get('name'), msg.unparsed_msg);
+                this.alert('highlight');
+
+            } else {
+                // If this is the active panel, send an alert out
+                if (this.model.isActive()) {
+                    _kiwi.app.view.alertWindow('* ' + _kiwi.global.i18n.translate('client_views_panel_activity').fetch());
+                }
+                this.alert('activity');
+            }
+
+            if (this.model.isQuery() && !this.model.isActive()) {
+                _kiwi.app.view.alertWindow('* ' + _kiwi.global.i18n.translate('client_views_panel_activity').fetch());
+
+                // Highlights have already been dealt with above
+                if (!msg.is_highlight) {
+                    _kiwi.app.view.favicon.newHighlight();
+                }
+
+                _kiwi.app.view.showNotification(this.model.get('name'), msg.unparsed_msg);
+                _kiwi.app.view.playSound('highlight');
+            }
+
+            // Update the activity counters
+            (function () {
+                // Only inrement the counters if we're not the active panel
+                if (this.model.isActive()) return;
+
+                var count_all_activity = _kiwi.global.settings.get('count_all_activity'),
+                    exclude_message_types, new_count;
+
+                // Set the default config value
+                if (typeof count_all_activity === 'undefined') {
+                    count_all_activity = false;
+                }
+
+                // Do not increment the counter for these message types
+                exclude_message_types = [
+                    'action join',
+                    'action quit',
+                    'action part',
+                    'action kick',
+                    'action nick',
+                    'action mode'
+                ];
+
+                if (count_all_activity || _.indexOf(exclude_message_types, msg.type) === -1) {
+                    new_count = this.model.get('activity_counter') || 0;
+                    new_count++;
+                    this.model.set('activity_counter', new_count);
+                }
+
+            }).apply(this);
+
+            if(this.model.isActive()) this.scrollToBottom();
+
+            // Make sure our DOM isn't getting too large (Acts as scrollback)
+            this.msg_count++;
+            if (this.msg_count > (parseInt(_kiwi.global.settings.get('scrollback'), 10) || 250)) {
+                $('.msg:first', this.$messages).remove();
+                this.msg_count--;
+            }
+        }, this));
+    },
+
+
+    // Let nicks be clickable + colourise within messages
+    parseMessageNicks: function(word, colourise) {
+        var members, member, style = '';
+
+        members = this.model.get('members');
+        if (!members) {
+            return;
         }
 
-        // Escape any HTML that may be in here
-        msg.msg =  $('<div />').text(msg.msg).html();
+        member = members.getByNick(word);
+        if (!member) {
+            return;
+        }
 
-        // Make the channels clickable
-        if ((network = this.model.get('network'))) {
-            re = new RegExp('(?:^|\\s)([' + escapeRegex(network.get('channel_prefix')) + '][^ ,\\007]+)', 'g');
-            msg.msg = msg.msg.replace(re, function (match) {
-                return '<a class="chan" data-channel="' + _.escape(match.trim()) + '">' + _.escape(match.trim()) + '</a>';
-            });
+        if (colourise !== false) {
+            // Use the nick from the member object so the style matches the letter casing
+            style = this.getNickStyles(member.get('nick')).asCssString();
+        }
+
+        return _.template('<span class="inline-nick" style="<%- style %>;cursor:pointer;" data-nick="<%- nick %>"><%- nick %></span>', {
+            nick: word,
+            style: style
+        });
+
+    },
+
+
+    // Make channels clickable
+    parseMessageChannels: function(word) {
+        var re,
+            parsed = false,
+            network = this.model.get('network');
+
+        if (!network) {
+            return;
         }
 
+        re = new RegExp('(^|\\s)([' + escapeRegex(network.get('channel_prefix')) + '][^ ,\\007]+)', 'g');
+
+        if (!word.match(re)) {
+            return parsed;
+        }
 
-        // Parse any links found
-        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) {
+        parsed = word.replace(re, function (m1, m2) {
+            return m2 + '<a class="chan" data-channel="' + _.escape(m1.trim()) + '">' + _.escape(m1.trim()) + '</a>';
+        });
+
+        return parsed;
+    },
+
+
+    parseMessageUrls: function(word) {
+        var found_a_url = false,
+            parsed_url;
+
+        parsed_url = word.replace(/^(([A-Za-z][A-Za-z0-9\-]*\:\/\/)|(www\.))([\w.\-]+)([a-zA-Z]{2,6})(:[0-9]+)?(\/[\w!:.?$'()[\]*,;~+=&%@!\-\/]*)?(#.*)?$/gi, function (url) {
             var nice = url,
                 extra_html = '';
 
+            // Don't allow javascript execution
+            if (url.match(/^javascript:/)) {
+                return url;
+            }
+
+            found_a_url = true;
+
             // Add the http if no protoocol was found
             if (url.match(/^www\./)) {
                 url = 'http://' + url;
@@ -98,50 +235,123 @@ _kiwi.view.Channel = _kiwi.view.Panel.extend({
             extra_html = _kiwi.view.MediaMessage.buildHtml(url);
 
             // Make the link clickable
-            return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url + '">' + nice + '</a>' + extra_html;
+            return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url.replace(/"/g, '%22') + '">' + _.escape(nice) + '</a>' + extra_html;
         });
 
+        return found_a_url ? parsed_url : false;
+    },
 
-        // Convert IRC formatting into HTML formatting
-        msg.msg = formatIRCMsg(msg.msg);
 
-        // Replace text emoticons with images
-        if (_kiwi.global.settings.get('show_emoticons')) {
-            msg.msg = emoticonFromText(msg.msg);
+    // Sgnerate a css style for a nick
+    getNickStyles: function(nick) {
+        var ret, colour, nick_int = 0, rgb, nick_lightness;
+
+        // Get a colour from a nick (Method based on IRSSIs nickcolor.pl)
+        _.map(nick.split(''), function (i) { nick_int += i.charCodeAt(0); });
+
+        nick_lightness = (_.find(_kiwi.app.themes, function (theme) {
+            return theme.name.toLowerCase() === _kiwi.global.settings.get('theme').toLowerCase();
+        }) || {}).nick_lightness;
+
+        if (typeof nick_lightness !== 'number') {
+            nick_lightness = 35;
+        } else {
+            nick_lightness = Math.max(0, Math.min(100, nick_lightness));
+        }
+
+        rgb = hsl2rgb(nick_int % 255, 70, nick_lightness);
+        rgb = rgb[2] | (rgb[1] << 8) | (rgb[0] << 16);
+        colour = '#' + rgb.toString(16);
+
+        ret = {color: colour};
+        ret.asCssString = function() {
+            return _.reduce(this, function(result, item, key){
+                return result + key + ':' + item + ';';
+            }, '');
+        };
+
+        return ret;
+    },
+
+
+    // Takes an IRC message object and parses it for displaying
+    generateMessageDisplayObj: function(msg) {
+        var nick_hex, time_difference,
+            message_words,
+            sb = this.model.get('scrollback'),
+            prev_msg = sb[sb.length-2],
+            hour, pm, am_pm_locale_key;
+
+        // Clone the msg object so we dont modify the original
+        msg = _.clone(msg);
+
+        // Defaults
+        msg.css_classes = '';
+        msg.nick_style = '';
+        msg.is_highlight = false;
+        msg.time_string = '';
+
+
+        // Nick highlight detecting
+        var nick = _kiwi.app.connections.active_connection.get('nick');
+        if ((new RegExp('(^|\\W)(' + escapeRegex(nick) + ')(\\W|$)', 'i')).test(msg.msg)) {
+            // Do not highlight the user's own input
+            if (msg.nick.localeCompare(nick) !== 0) {
+                msg.is_highlight = true;
+                msg.css_classes += ' highlight';
+            }
         }
 
-        // Add some colours to the nick (Method based on IRSSIs nickcolor.pl)
-        nick_colour_hex = (function (nick) {
-            var nick_int = 0, rgb;
+        message_words = msg.msg.split(' ');
+        message_words = _.map(message_words, function(word) {
+            var parsed_word;
+
+            parsed_word = this.parseMessageUrls(word);
+            if (typeof parsed_word === 'string') return parsed_word;
 
-            _.map(nick.split(''), function (i) { nick_int += i.charCodeAt(0); });
-            rgb = hsl2rgb(nick_int % 255, 70, 35);
-            rgb = rgb[2] | (rgb[1] << 8) | (rgb[0] << 16);
+            parsed_word = this.parseMessageChannels(word);
+            if (typeof parsed_word === 'string') return parsed_word;
+
+            parsed_word = this.parseMessageNicks(word, (msg.type === 'privmsg'));
+            if (typeof parsed_word === 'string') return parsed_word;
+
+            parsed_word = _.escape(word);
+
+            // Replace text emoticons with images
+            if (_kiwi.global.settings.get('show_emoticons')) {
+                parsed_word = emoticonFromText(parsed_word);
+            }
 
-            return '#' + rgb.toString(16);
-        })(msg.nick);
+            return parsed_word;
+        }, this);
 
-        msg.nick_style = 'color:' + nick_colour_hex + ';';
+        msg.unparsed_msg = msg.msg;
+        msg.msg = message_words.join(' ');
+
+        // Convert IRC formatting into HTML formatting
+        msg.msg = formatIRCMsg(msg.msg);
+
+        // Add some style to the nick
+        msg.nick_style = this.getNickStyles(msg.nick).asCssString();
 
         // Generate a hex string from the nick to be used as a CSS class name
-        nick_hex = msg.nick_css_class = '';
+        nick_hex = '';
         if (msg.nick) {
             _.map(msg.nick.split(''), function (char) {
                 nick_hex += char.charCodeAt(0).toString(16);
             });
-            msg_css_classes += ' nick_' + nick_hex;
+            msg.css_classes += ' nick_' + nick_hex;
         }
 
         if (prev_msg) {
             // Time difference between this message and the last (in minutes)
             time_difference = (msg.time.getTime() - prev_msg.time.getTime())/1000/60;
             if (prev_msg.nick === msg.nick && time_difference < 1) {
-                msg_css_classes += ' repeated_nick';
+                msg.css_classes += ' repeated_nick';
             }
         }
 
         // Build up and add the line
-        msg.msg_css_classes = msg_css_classes;
         if (_kiwi.global.settings.get('use_24_hour_timestamps')) {
             msg.time_string = msg.time.getHours().toString().lpad(2, "0") + ":" + msg.time.getMinutes().toString().lpad(2, "0") + ":" + msg.time.getSeconds().toString().lpad(2, "0");
         } else {
@@ -152,143 +362,115 @@ _kiwi.view.Channel = _kiwi.view.Panel.extend({
             if (hour === 0)
                 hour = 12;
 
-            if (pm) {
-                msg.time_string = _kiwi.global.i18n.translate('client_views_panel_timestamp_pm').fetch(hour + ":" + msg.time.getMinutes().toString().lpad(2, "0") + ":" + msg.time.getSeconds().toString().lpad(2, "0"));
-            } else {
-                msg.time_string = _kiwi.global.i18n.translate('client_views_panel_timestamp_am').fetch(hour + ":" + msg.time.getMinutes().toString().lpad(2, "0") + ":" + msg.time.getSeconds().toString().lpad(2, "0"));
-            }
+            am_pm_locale_key = pm ?
+                'client_views_panel_timestamp_pm' :
+                'client_views_panel_timestamp_am';
+
+            msg.time_string = translateText(am_pm_locale_key, hour + ":" + msg.time.getMinutes().toString().lpad(2, "0") + ":" + msg.time.getSeconds().toString().lpad(2, "0"));
         }
-        line_msg = '<div class="msg <%= type %> <%= msg_css_classes %>"><div class="time"><%- time_string %></div><div class="nick" style="<%= nick_style %>"><%- nick %></div><div class="text" style="<%= style %>"><%= msg %> </div></div>';
-        this.$messages.append(_.template(line_msg, msg));
 
-        // Activity/alerts based on the type of new message
-        if (msg.type.match(/^action /)) {
-            this.alert('action');
+        return msg;
+    },
 
-        } else if (is_highlight) {
-            _kiwi.app.view.alertWindow('* ' + _kiwi.global.i18n.translate('client_views_panel_activity').fetch());
-            _kiwi.app.view.favicon.newHighlight();
-            _kiwi.app.view.playSound('highlight');
-            _kiwi.app.view.showNotification(this.model.get('name'), msg.msg);
-            this.alert('highlight');
 
-        } else {
-            // If this is the active panel, send an alert out
-            if (this.model.isActive()) {
-                _kiwi.app.view.alertWindow('* ' + _kiwi.global.i18n.translate('client_views_panel_activity').fetch());
-            }
-            this.alert('activity');
+    topic: function (topic) {
+        if (typeof topic !== 'string' || !topic) {
+            topic = this.model.get("topic");
         }
 
-        if (this.model.isQuery() && !this.model.isActive()) {
-            _kiwi.app.view.alertWindow('* ' + _kiwi.global.i18n.translate('client_views_panel_activity').fetch());
+        this.model.addMsg('', styleText('channel_topic', {text: topic, channel: this.model.get('name')}), 'topic');
 
-            // Highlights have already been dealt with above
-            if (!is_highlight) {
-                _kiwi.app.view.favicon.newHighlight();
-            }
+        // If this is the active channel then update the topic bar
+        if (_kiwi.app.panels().active === this.model) {
+            _kiwi.app.topicbar.setCurrentTopicFromChannel(this.model);
+        }
+    },
 
-            _kiwi.app.view.showNotification(this.model.get('name'), msg.msg);
-            _kiwi.app.view.playSound('highlight');
+    topicSetBy: function (topic) {
+        // If this is the active channel then update the topic bar
+        if (_kiwi.app.panels().active === this.model) {
+            _kiwi.app.topicbar.setCurrentTopicFromChannel(this.model);
         }
+    },
 
-        // Update the activity counters
-        (function () {
-            // Only inrement the counters if we're not the active panel
-            if (this.model.isActive()) return;
+    // Click on a nickname
+    nickClick: function (event) {
+        var $target = $(event.currentTarget),
+            nick,
+            members = this.model.get('members'),
+            member;
 
-            var $act = this.model.tab.find('.activity'),
-                count_all_activity = _kiwi.global.settings.get('count_all_activity'),
-                exclude_message_types;
+        event.stopPropagation();
 
-            // Set the default config value
-            if (typeof count_all_activity === 'undefined') {
-                count_all_activity = false;
-            }
+        // Check this current element for a nick before resorting to the main message
+        // (eg. inline nicks has the nick on its own element within the message)
+        nick = $target.data('nick');
+        if (!nick) {
+            nick = $target.parent('.msg').data('message').nick;
+        }
 
-            // Do not increment the counter for these message types
-            exclude_message_types = [
-                'action join',
-                'action quit',
-                'action part',
-                'action kick',
-                'action nick',
-                'action mode'
-            ];
-
-            if (count_all_activity || _.indexOf(exclude_message_types, msg.type) === -1) {
-                $act.text((parseInt($act.text(), 10) || 0) + 1);
-            }
+        // Make sure this nick is still in the channel
+        member = members ? members.getByNick(nick) : null;
+        if (!member) {
+            return;
+        }
 
-            if ($act.text() === '0') {
-                $act.addClass('zero');
-            } else {
-                $act.removeClass('zero');
-            }
-        }).apply(this);
+        _kiwi.global.events.emit('nick:select', {target: $target, member: member, source: 'message'})
+        .then(_.bind(this.openUserMenuForNick, this, $target, member));
+    },
 
-        if(this.model.isActive()) this.scrollToBottom();
 
-        // Make sure our DOM isn't getting too large (Acts as scrollback)
-        this.msg_count++;
-        if (this.msg_count > (parseInt(_kiwi.global.settings.get('scrollback'), 10) || 250)) {
-            $('.msg:first', this.$messages).remove();
-            this.msg_count--;
+    updateLastSeenMarker: function() {
+        if (this.model.isActive()) {
+            // Remove the previous last seen classes
+            this.$(".last_seen").removeClass("last_seen");
+
+            // Mark the last message the user saw
+            this.$messages.children().last().addClass("last_seen");
         }
     },
 
 
-    topic: function (topic) {
-        if (typeof topic !== 'string' || !topic) {
-            topic = this.model.get("topic");
-        }
+    openUserMenuForNick: function ($target, member) {
+        var members = this.model.get('members'),
+            are_we_an_op = !!members.getByNick(_kiwi.app.connections.active_connection.get('nick')).get('is_op'),
+            userbox, menubox;
 
-        this.model.addMsg('', '== ' + _kiwi.global.i18n.translate('client_views_channel_topic').fetch(this.model.get('name'), topic), 'topic');
+        userbox = new _kiwi.view.UserBox();
+        userbox.setTargets(member, this.model);
+        userbox.displayOpItems(are_we_an_op);
 
-        // If this is the active channel then update the topic bar
-        if (_kiwi.app.panels().active === this) {
-            _kiwi.app.topicbar.setCurrentTopic(this.model.get("topic"));
-        }
-    },
+        menubox = new _kiwi.view.MenuBox(member.get('nick') || 'User');
+        menubox.addItem('userbox', userbox.$el);
+        menubox.showFooter(false);
 
-    // Click on a nickname
-    nickClick: function (event) {
-        var nick = $(event.currentTarget).text(),
-            members = this.model.get('members'),
-            are_we_an_op = !!members.getByNick(_kiwi.app.connections.active_connection.get('nick')).get('is_op'),
-            member, query, userbox, menubox;
-
-        if (members) {
-            member = members.getByNick(nick);
-            if (member) {
-                userbox = new _kiwi.view.UserBox();
-                userbox.setTargets(member, this.model);
-                userbox.displayOpItems(are_we_an_op);
-
-                menubox = new _kiwi.view.MenuBox(member.get('nick') || 'User');
-                menubox.addItem('userbox', userbox.$el);
-                menubox.showFooter(false);
-                menubox.show();
-
-                // Position the userbox + menubox
-                (function() {
-                    var t = event.pageY,
-                        m_bottom = t + menubox.$el.outerHeight(),  // Where the bottom of menu will be
-                        memberlist_bottom = this.$el.parent().offset().top + this.$el.parent().outerHeight();
-
-                    // If the bottom of the userbox is going to be too low.. raise it
-                    if (m_bottom > memberlist_bottom){
-                        t = memberlist_bottom - menubox.$el.outerHeight();
-                    }
-
-                    // Set the new positon
-                    menubox.$el.offset({
-                        left: event.clientX,
-                        top: t
-                    });
-                }).call(this);
+        _kiwi.global.events.emit('usermenu:created', {menu: menubox, userbox: userbox, user: member})
+        .then(_.bind(function() {
+            menubox.show();
+
+            // Position the userbox + menubox
+            var target_offset = $target.offset(),
+                t = target_offset.top,
+                m_bottom = t + menubox.$el.outerHeight(),  // Where the bottom of menu will be
+                memberlist_bottom = this.$el.parent().offset().top + this.$el.parent().outerHeight();
+
+            // If the bottom of the userbox is going to be too low.. raise it
+            if (m_bottom > memberlist_bottom){
+                t = memberlist_bottom - menubox.$el.outerHeight();
             }
-        }
+
+            // Set the new positon
+            menubox.$el.offset({
+                left: target_offset.left,
+                top: t
+            });
+        }, this))
+        .catch(_.bind(function() {
+            userbox = null;
+
+            menu.dispose();
+            menu = null;
+        }, this));
     },
 
 
@@ -349,5 +531,5 @@ _kiwi.view.Channel = _kiwi.view.Panel.extend({
         if (!nick_class) return;
 
         $('.'+nick_class).removeClass('global_nick_highlight');
-    },
+    }
 });