X-Git-Url: https://vcs.fsf.org/?a=blobdiff_plain;f=client%2Fassets%2Fdev%2Fview.js;h=b104c7c002b47a805fd840d0b1985538fdefd7e4;hb=0e546dd4622fbb302b822dbc247f639d51a94c97;hp=64889d29cc89b72689bad617d3bdb0bb657cfae2;hpb=cee337bb57e62637ff0dc0a4a645d792a8755aa7;p=KiwiIRC.git diff --git a/client/assets/dev/view.js b/client/assets/dev/view.js index 64889d2..b104c7c 100644 --- a/client/assets/dev/view.js +++ b/client/assets/dev/view.js @@ -1,7 +1,7 @@ /*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 */ /*global kiwi */ -kiwi.view.MemberList = Backbone.View.extend({ +_kiwi.view.MemberList = Backbone.View.extend({ tagName: "ul", events: { "click .nick": "nickClick" @@ -14,19 +14,54 @@ kiwi.view.MemberList = Backbone.View.extend({ var $this = $(this.el); $this.empty(); this.model.forEach(function (member) { - $('
  • ' + member.get("prefix") + '' + member.get("nick") + '
  • ') + var prefix_css_class = (member.get('modes') || []).join(' '); + $('
  • ' + member.get("prefix") + '' + member.get("nick") + '
  • ') .appendTo($this) .data('member', member); }); }, - nickClick: function (x) { - var target = $(x.currentTarget).parent('li'), - member = target.data('member'), - userbox = new kiwi.view.UserBox(); + nickClick: function (event) { + var $target = $(event.currentTarget).parent('li'), + member = $target.data('member'), + userbox; + event.stopPropagation(); + + // If the userbox already exists here, hide it + if ($target.find('.userbox').length > 0) { + $('.userbox', this.$el).remove(); + return; + } + + userbox = new _kiwi.view.UserBox(); userbox.member = member; - $('.userbox', this.$el).remove(); - target.append(userbox.$el); + userbox.channel = this.model.channel; + + if (!this.model.getByNick(_kiwi.gateway.get('nick')).get('is_op')) { + userbox.$el.children('.if_op').remove(); + } + + var menu = new _kiwi.view.MenuBox(member.get('nick') || 'User'); + menu.addItem('userbox', userbox.$el); + menu.show(); + + // Position the userbox + menubox + (function() { + var t = event.pageY, + m_bottom = t + menu.$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 - menu.$el.outerHeight(); + } + + // Set the new positon + menu.$el.offset({ + left: _kiwi.app.view.$el.width() - menu.$el.outerWidth() - 20, + top: t + }); + }).call(this); }, show: function () { $('#memberlists').children().removeClass('active'); @@ -36,11 +71,17 @@ kiwi.view.MemberList = Backbone.View.extend({ -kiwi.view.UserBox = Backbone.View.extend({ +_kiwi.view.UserBox = Backbone.View.extend({ events: { 'click .query': 'queryClick', 'click .info': 'infoClick', - 'click .slap': 'slapClick' + 'click .slap': 'slapClick', + 'click .op': 'opClick', + 'click .deop': 'deopClick', + 'click .voice': 'voiceClick', + 'click .devoice': 'devoiceClick', + 'click .kick': 'kickClick', + 'click .ban': 'banClick' }, initialize: function () { @@ -48,22 +89,47 @@ kiwi.view.UserBox = Backbone.View.extend({ }, queryClick: function (event) { - var panel = new kiwi.model.Channel({name: this.member.get('nick')}); - panel.set('members', undefined); - kiwi.app.panels.add(panel); + var panel = new _kiwi.model.Query({name: this.member.get('nick')}); + _kiwi.app.panels.add(panel); panel.view.show(); }, infoClick: function (event) { - kiwi.app.controlbox.processInput('/whois ' + this.member.get('nick')); + _kiwi.app.controlbox.processInput('/whois ' + this.member.get('nick')); }, slapClick: function (event) { - kiwi.app.controlbox.processInput('/slap ' + this.member.get('nick')); + _kiwi.app.controlbox.processInput('/slap ' + this.member.get('nick')); + }, + + opClick: function (event) { + _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +o ' + this.member.get('nick')); + }, + + deopClick: function (event) { + _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' -o ' + this.member.get('nick')); + }, + + voiceClick: function (event) { + _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +v ' + this.member.get('nick')); + }, + + devoiceClick: function (event) { + _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' -v ' + this.member.get('nick')); + }, + + kickClick: function (event) { + // TODO: Enable the use of a custom kick message + _kiwi.app.controlbox.processInput('/kick ' + this.member.get('nick') + ' Bye!'); + }, + + banClick: function (event) { + // TODO: Set ban on host, not just on nick + _kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +b ' + this.member.get('nick') + '!*'); } }); -kiwi.view.NickChangeBox = Backbone.View.extend({ +_kiwi.view.NickChangeBox = Backbone.View.extend({ events: { 'submit': 'changeNick', 'click .cancel': 'close' @@ -75,10 +141,10 @@ kiwi.view.NickChangeBox = Backbone.View.extend({ render: function () { // Add the UI component and give it focus - kiwi.app.controlbox.$el.prepend(this.$el); + _kiwi.app.controlbox.$el.prepend(this.$el); this.$el.find('input').focus(); - this.$el.css('bottom', kiwi.app.controlbox.$el.outerHeight(true)); + this.$el.css('bottom', _kiwi.app.controlbox.$el.outerHeight(true)); }, close: function () { @@ -88,40 +154,67 @@ kiwi.view.NickChangeBox = Backbone.View.extend({ changeNick: function (event) { var that = this; - kiwi.gateway.changeNick(this.$el.find('input').val(), function (err, val) { + _kiwi.gateway.changeNick(this.$el.find('input').val(), function (err, val) { that.close(); }); return false; } }); -kiwi.view.ServerSelect = function () { +_kiwi.view.ServerSelect = function () { // Are currently showing all the controlls or just a nick_change box? var state = 'all'; var model = Backbone.View.extend({ events: { 'submit form': 'submitForm', - 'click .show_more': 'showMore' + 'click .show_more': 'showMore', + 'change .have_pass input': 'showPass' }, initialize: function () { + var that = this; + this.$el = $($('#tmpl_server_select').html()); - kiwi.gateway.bind('onconnect', this.networkConnected, this); - kiwi.gateway.bind('connecting', this.networkConnecting, this); + // Remove the 'more' link if the server has disabled server changing + if (_kiwi.app.server_settings && _kiwi.app.server_settings.connection) { + if (!_kiwi.app.server_settings.connection.allow_change) { + this.$el.find('.show_more').remove(); + this.$el.addClass('single_server'); + } + } + - kiwi.gateway.bind('onirc_error', function (data) { + _kiwi.gateway.bind('onconnect', this.networkConnected, this); + _kiwi.gateway.bind('connecting', this.networkConnecting, this); + + _kiwi.gateway.bind('onirc_error', function (data) { $('button', this.$el).attr('disabled', null); if (data.error == 'nickname_in_use') { this.setStatus('Nickname already taken'); this.show('nick_change'); } + + if (data.error == 'password_mismatch') { + this.setStatus('Incorrect Password'); + this.show('nick_change'); + that.$el.find('.password').select(); + } }, this); }, submitForm: function (event) { + event.preventDefault(); + + // Make sure a nick is chosen + if (!$('input.nick', this.$el).val().trim()) { + this.setStatus('Select a nickname first!'); + $('input.nick', this.$el).select(); + return; + } + if (state === 'nick_change') { this.submitNickChange(event); } else { @@ -129,7 +222,7 @@ kiwi.view.ServerSelect = function () { } $('button', this.$el).attr('disabled', 1); - return false; + return; }, submitLogin: function (event) { @@ -137,29 +230,38 @@ kiwi.view.ServerSelect = function () { if ($('button', this.$el).attr('disabled')) return; var values = { - nick: $('.nick', this.$el).val(), - server: $('.server', this.$el).val(), - port: $('.port', this.$el).val(), - ssl: $('.ssl', this.$el).prop('checked'), - password: $('.password', this.$el).val(), - channel: $('.channel', this.$el).val() + nick: $('input.nick', this.$el).val(), + server: $('input.server', this.$el).val(), + port: $('input.port', this.$el).val(), + ssl: $('input.ssl', this.$el).prop('checked'), + password: $('input.password', this.$el).val(), + channel: $('input.channel', this.$el).val(), + channel_key: $('input.channel_key', this.$el).val() }; this.trigger('server_connect', values); }, submitNickChange: function (event) { - kiwi.gateway.changeNick($('.nick', this.$el).val()); + _kiwi.gateway.changeNick($('input.nick', this.$el).val()); this.networkConnecting(); }, + showPass: function (event) { + if (this.$el.find('tr.have_pass input').is(':checked')) { + this.$el.find('tr.pass').show().find('input').focus(); + } else { + this.$el.find('tr.pass').hide().find('input').val(''); + } + }, + showMore: function (event) { $('.more', this.$el).slideDown('fast'); - $('.server', this.$el).select(); + $('input.server', this.$el).select(); }, populateFields: function (defaults) { - var nick, server, channel; + var nick, server, port, channel, channel_key, ssl, password; defaults = defaults || {}; @@ -169,13 +271,15 @@ kiwi.view.ServerSelect = function () { ssl = defaults.ssl || 0; password = defaults.password || ''; channel = defaults.channel || ''; - - $('.nick', this.$el).val(nick); - $('.server', this.$el).val(server); - $('.port', this.$el).val(port); - $('.ssl', this.$el).prop('checked', ssl); - $('.password', this.$el).val(password); - $('.channel', this.$el).val(channel); + channel_key = defaults.channel_key || ''; + + $('input.nick', this.$el).val(nick); + $('input.server', this.$el).val(server); + $('input.port', this.$el).val(port); + $('input.ssl', this.$el).prop('checked', ssl); + $('input.password', this.$el).val(password); + $('input.channel', this.$el).val(channel); + $('input.channel_key', this.$el).val(channel_key); }, hide: function () { @@ -196,6 +300,7 @@ kiwi.view.ServerSelect = function () { } else if (new_state === 'nick_change') { $('.more', this.$el).hide(); $('.show_more', this.$el).hide(); + $('input.nick', this.$el).select(); } state = new_state; @@ -205,7 +310,7 @@ kiwi.view.ServerSelect = function () { $('.status', this.$el) .text(text) .attr('class', 'status') - .addClass(class_name) + .addClass(class_name||'') .show(); }, clearStatus: function () { @@ -233,11 +338,14 @@ kiwi.view.ServerSelect = function () { }; -kiwi.view.Panel = Backbone.View.extend({ +_kiwi.view.Panel = Backbone.View.extend({ tagName: "div", className: "messages", events: { - "click .chan": "chanClick" + "click .chan": "chanClick", + 'click .media .open': 'mediaClick', + 'mouseenter .msg .nick': 'msgEnter', + 'mouseleave .msg .nick': 'msgLeave' }, initialize: function (options) { @@ -266,39 +374,54 @@ kiwi.view.Panel = Backbone.View.extend({ }, render: function () { + var that = this; + this.$el.empty(); - this.model.get("backscroll").forEach(this.newMsg); + _.each(this.model.get('scrollback'), function (msg) { + that.newMsg(msg); + }); }, + newMsg: function (msg) { - // TODO: make sure that the message pane is scrolled to the bottom (Or do we? ~Darren) var re, line_msg, $this = this.$el, - nick_colour_hex; + nick_colour_hex, nick_hex, is_highlight, msg_css_classes = ''; + + // Nick highlight detecting + if ((new RegExp('\\b' + _kiwi.gateway.get('nick') + '\\b', 'i')).test(msg.msg)) { + is_highlight = true; + msg_css_classes += ' highlight'; + } // Escape any HTML that may be in here msg.msg = $('
    ').text(msg.msg).html(); // Make the channels clickable - re = new RegExp('\\B([' + kiwi.gateway.get('channel_prefix') + '][^ ,.\\007]+)', 'g'); + re = new RegExp('(?:^|\\s)([' + _kiwi.gateway.get('channel_prefix') + '][^ ,.\\007]+)', 'g'); msg.msg = msg.msg.replace(re, function (match) { - return '' + match + ''; + return '' + match + ''; }); - // Make links clickable - msg.msg = msg.msg.replace(/((https?\:\/\/|ftp\:\/\/)|(www\.))(\S+)(\w{2,4})(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]*))?/gi, function (url) { - var nice; + // Parse any links found + msg.msg = msg.msg.replace(/(([A-Za-z0-9\-]+\:\/\/)|(www\.))([\w.\-]+)([a-zA-Z]{2,6})(:[0-9]+)?(\/[\w#!:.?$'()[\]*,;~+=&%@!\-\/]*)?/gi, function (url) { + var nice = url, + extra_html = ''; - // Add the http is no protoocol was found + // Add the http if no protoocol was found if (url.match(/^www\./)) { url = 'http://' + url; } - nice = url; + // Shorten the displayed URL if it's going to be too long if (nice.length > 100) { nice = nice.substr(0, 100) + '...'; } - return '' + nice + ''; + // Get any media HTML if supported + extra_html = _kiwi.view.MediaMessage.buildHtml(url); + + // Make the link clickable + return '' + nice + ' ' + extra_html; }); @@ -319,41 +442,124 @@ kiwi.view.Panel = Backbone.View.extend({ msg.nick_style = 'color:' + nick_colour_hex + ';'; + // Generate a hex string from the nick to be used as a CSS class name + nick_hex = msg.nick_css_class = ''; + if (msg.nick) { + _.map(msg.nick.split(''), function (char) { + nick_hex += char.charCodeAt(0).toString(16); + }); + msg_css_classes += ' nick_' + nick_hex; + } + // Build up and add the line - line_msg = '
    <%- time %>
    <%- nick %>
    <%= msg %>
    '; + msg.msg_css_classes = msg_css_classes; + line_msg = '
    <%- time %>
    <%- nick %>
    <%= msg %>
    '; $this.append(_.template(line_msg, msg)); // Activity/alerts based on the type of new message if (msg.type.match(/^action /)) { this.alert('action'); - } else if (msg.msg.indexOf(kiwi.gateway.get('nick')) > -1) { - kiwi.app.view.alertWindow('* People are talking!'); + + } else if (is_highlight) { + _kiwi.app.view.alertWindow('* People are talking!'); + _kiwi.app.view.playSound('highlight'); this.alert('highlight'); + } else { // If this is the active panel, send an alert out if (this.model.isActive()) { - kiwi.app.view.alertWindow('* People are talking!'); + _kiwi.app.view.alertWindow('* People are talking!'); } this.alert('activity'); } + if (this.model.isQuery() && !this.model.isActive()) { + _kiwi.app.view.alertWindow('* People are talking!'); + _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 $act = this.model.tab.find('.activity'); + $act.text((parseInt($act.text(), 10) || 0) + 1); + if ($act.text() === '0') { + $act.addClass('zero'); + } else { + $act.removeClass('zero'); + } + }).apply(this); + this.scrollToBottom(); // Make sure our DOM isn't getting too large (Acts as scrollback) this.msg_count++; - if (this.msg_count > 250) { + if (this.msg_count > (parseInt(_kiwi.global.settings.get('scrollback'), 10) || 250)) { $('.msg:first', this.$el).remove(); this.msg_count--; } }, chanClick: function (event) { if (event.target) { - kiwi.gateway.join($(event.target).text()); + _kiwi.gateway.join($(event.target).data('channel')); } else { // IE... - kiwi.gateway.join($(event.srcElement).text()); + _kiwi.gateway.join($(event.srcElement).data('channel')); } }, + + mediaClick: function (event) { + var $media = $(event.target).parents('.media'); + var media_message; + + if ($media.data('media')) { + media_message = $media.data('media'); + } else { + media_message = new _kiwi.view.MediaMessage({el: $media[0]}); + $media.data('media', media_message); + } + + $media.data('media', media_message); + + media_message.open(); + }, + + // Cursor hovers over a message + msgEnter: function (event) { + var nick_class; + + // Find a valid class that this element has + _.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) { + if (css_class.match(/^nick_[a-z0-9]+/i)) { + nick_class = css_class; + } + }); + + // If no class was found.. + if (!nick_class) return; + + $('.'+nick_class).addClass('global_nick_highlight'); + }, + + // Cursor leaves message + msgLeave: function (event) { + var nick_class; + + // Find a valid class that this element has + _.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) { + if (css_class.match(/^nick_[a-z0-9]+/i)) { + nick_class = css_class; + } + }); + + // If no class was found.. + if (!nick_class) return; + + $('.'+nick_class).removeClass('global_nick_highlight'); + }, + show: function () { var $this = this.$el; @@ -364,26 +570,29 @@ kiwi.view.Panel = Backbone.View.extend({ // Show this panels memberlist var members = this.model.get("members"); if (members) { - $('#memberlists').show(); + $('#memberlists').removeClass('disabled'); members.view.show(); } else { // Memberlist not found for this panel, hide any active ones - $('#memberlists').hide().children().removeClass('active'); + $('#memberlists').addClass('disabled').children().removeClass('active'); } - kiwi.app.view.doLayout(); + _kiwi.app.view.doLayout(); - this.scrollToBottom(); + // Remove any alerts and activity counters for this panel this.alert('none'); + this.model.tab.find('.activity').text('0').addClass('zero'); this.trigger('active', this.model); - kiwi.app.panels.trigger('active', this.model); + _kiwi.app.panels.trigger('active', this.model, _kiwi.app.panels.active); + + this.scrollToBottom(true); }, alert: function (level) { // No need to highlight if this si the active panel - if (this.model == kiwi.app.panels.active) return; + if (this.model == _kiwi.app.panels.active) return; var types, type_idx; types = ['none', 'action', 'activity', 'highlight']; @@ -418,23 +627,42 @@ kiwi.view.Panel = Backbone.View.extend({ // Scroll to the bottom of the panel - scrollToBottom: function () { - // TODO: Don't scroll down if we're scrolled up the panel a little - this.$container[0].scrollTop = this.$container[0].scrollHeight; + scrollToBottom: function (force_down) { + // If this isn't the active panel, don't scroll + if (this.model !== _kiwi.app.panels.active) return; + + // Don't scroll down if we're scrolled up the panel a little + if (force_down || this.$container.scrollTop() + this.$container.height() > this.$el.outerHeight() - 150) { + this.$container[0].scrollTop = this.$container[0].scrollHeight; + } } }); -kiwi.view.Applet = kiwi.view.Panel.extend({ +_kiwi.view.Applet = _kiwi.view.Panel.extend({ className: 'applet', initialize: function (options) { this.initializePanel(options); } }); -kiwi.view.Channel = kiwi.view.Panel.extend({ +_kiwi.view.Channel = _kiwi.view.Panel.extend({ initialize: function (options) { this.initializePanel(options); this.model.bind('change:topic', this.topic, this); + + // Only show the loader if this is a channel (ie. not a query) + if (this.model.isChannel()) { + this.$el.append('
    Joining channel..
    '); + } + }, + + // Override the existing newMsg() method to remove the joining channel loader + newMsg: function () { + this.$el.find('.initial_loader').slideUp(function () { + $(this).remove(); + }); + + return this.constructor.__super__.newMsg.apply(this, arguments); }, topic: function (topic) { @@ -445,14 +673,16 @@ kiwi.view.Channel = kiwi.view.Panel.extend({ this.model.addMsg('', '== Topic for ' + this.model.get('name') + ' is: ' + topic, 'topic'); // 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")); + if (_kiwi.app.panels.active === this) { + _kiwi.app.topicbar.setCurrentTopic(this.model.get("topic")); } } }); -// Model for this = kiwi.model.PanelList -kiwi.view.Tabs = Backbone.View.extend({ +// Model for this = _kiwi.model.PanelList +_kiwi.view.Tabs = Backbone.View.extend({ + tagName: 'ul', + events: { 'click li': 'tabClick', 'click li .part': 'partClick' @@ -465,22 +695,24 @@ kiwi.view.Tabs = Backbone.View.extend({ this.model.on('active', this.panelActive, this); - this.tabs_applets = $('ul.applets', this.$el); - this.tabs_msg = $('ul.channels', this.$el); - - kiwi.gateway.on('change:name', function (gateway, new_val) { + this.model.network.on('change:name', function (network, new_val) { $('span', this.model.server.tab).text(new_val); }, this); + + this.$tab_container = $('#kiwi .panellist.channels'); + this.$tab_container.append(this.$el); }, + render: function () { var that = this; - this.tabs_msg.empty(); + this.$el.empty(); // Add the server tab first this.model.server.tab - .data('panel_id', this.model.server.cid) - .appendTo(this.tabs_msg); + .data('panel', this.model.server) + .data('connection_id', this.model.network.get('connection_id')) + .appendTo(this.$el); // Go through each panel adding its tab this.model.forEach(function (panel) { @@ -488,11 +720,12 @@ kiwi.view.Tabs = Backbone.View.extend({ if (panel == that.model.server) return; panel.tab - .data('panel_id', panel.cid) - .appendTo(panel.isApplet() ? this.tabs_applets : this.tabs_msg); + .data('panel', panel) + .appendTo(that.$el); + //.appendTo(panel.isApplet() ? this.tabs_applets : this.tabs_msg); }); - kiwi.app.view.doLayout(); + _kiwi.app.view.doLayout(); }, updateTabTitle: function (panel, new_title) { @@ -501,43 +734,45 @@ kiwi.view.Tabs = Backbone.View.extend({ panelAdded: function (panel) { // Add a tab to the panel - panel.tab = $('
  • ' + (panel.get('title') || panel.get('name')) + '
  • '); + panel.tab = $('
  • ' + (panel.get('title') || panel.get('name')) + '
  • '); if (panel.isServer()) { panel.tab.addClass('server'); + panel.tab.addClass('icon-nonexistant'); } - panel.tab.data('panel_id', panel.cid) - .appendTo(panel.isApplet() ? this.tabs_applets : this.tabs_msg); + panel.tab.data('panel', panel) + .data('connection_id', this.model.network.get('connection_id')) + .appendTo(this.$el); + //.appendTo(panel.isApplet() ? this.tabs_applets : this.tabs_msg); panel.bind('change:title', this.updateTabTitle); - kiwi.app.view.doLayout(); + _kiwi.app.view.doLayout(); }, panelRemoved: function (panel) { panel.tab.remove(); delete panel.tab; - kiwi.app.view.doLayout(); + _kiwi.app.view.doLayout(); }, - panelActive: function (panel) { + panelActive: function (panel, previously_active_panel) { // Remove any existing tabs or part images $('.part', this.$el).remove(); - this.tabs_applets.children().removeClass('active'); - this.tabs_msg.children().removeClass('active'); + this.$tab_container.find('.active').removeClass('active'); panel.tab.addClass('active'); // Only show the part image on non-server tabs if (!panel.isServer()) { - panel.tab.append(''); + panel.tab.append(''); } }, tabClick: function (e) { var tab = $(e.currentTarget); - - var panel = this.model.getByCid(tab.data('panel_id')); + + var panel = tab.data('panel'); if (!panel) { // A panel wasn't found for this tab... wadda fuck return; @@ -548,26 +783,26 @@ kiwi.view.Tabs = Backbone.View.extend({ partClick: function (e) { var tab = $(e.currentTarget).parent(); - var panel = this.model.getByCid(tab.data('panel_id')); + var panel = this.model.getByCid(tab.data('panel')); // Only need to part if it's a channel // If the nicklist is empty, we haven't joined the channel as yet if (panel.isChannel() && panel.get('members').models.length > 0) { - kiwi.gateway.part(panel.get('name')); + this.model.network.gateway.part(panel.get('name')); } else { panel.close(); } }, next: function () { - var next = kiwi.app.panels.active.tab.next(); - if (!next.length) next = $('li:first', this.tabs_msgs); + var next = this.$tab_container.find('.active').next(); + if (!next.length) next = $('li:first', this.$tab_container); next.click(); }, prev: function () { - var prev = kiwi.app.panels.active.tab.prev(); - if (!prev.length) prev = $('li:last', this.tabs_msgs); + var prev = this.$tab_container.find('.active').prev(); + if (!prev.length) prev = $('li:last', this.$tab_container); prev.click(); } @@ -575,14 +810,23 @@ kiwi.view.Tabs = Backbone.View.extend({ -kiwi.view.TopicBar = Backbone.View.extend({ +_kiwi.view.TopicBar = Backbone.View.extend({ events: { 'keydown div': 'process' }, initialize: function () { - kiwi.app.panels.bind('active', function (active_panel) { - this.setCurrentTopic(active_panel.get('topic')); + _kiwi.app.panels.bind('active', function (active_panel) { + // If it's a channel topic, update and make editable + if (active_panel.isChannel()) { + this.setCurrentTopic(active_panel.get('topic') || ''); + this.$el.find('div').attr('contentEditable', true); + + } else { + // Not a channel topic.. clear and make uneditable + this.$el.find('div').attr('contentEditable', false) + .text(''); + } }, this); }, @@ -590,13 +834,16 @@ kiwi.view.TopicBar = Backbone.View.extend({ var inp = $(ev.currentTarget), inp_val = inp.text(); - if (kiwi.app.panels.active.isChannel()) { - if (ev.keyCode !== 13) return; + // Only allow topic editing if this is a channel panel + if (!_kiwi.app.panels.active.isChannel()) { + return false; + } - kiwi.gateway.topic(kiwi.app.panels.active.get('name'), inp_val); + // If hit return key, update the current topic + if (ev.keyCode === 13) { + _kiwi.gateway.topic(_kiwi.app.panels.active.get('name'), inp_val); + return false; } - - return false; }, setCurrentTopic: function (new_topic) { @@ -609,9 +856,9 @@ kiwi.view.TopicBar = Backbone.View.extend({ -kiwi.view.ControlBox = Backbone.View.extend({ +_kiwi.view.ControlBox = Backbone.View.extend({ events: { - 'keydown input.inp': 'process', + 'keydown .inp': 'process', 'click .nick': 'showNickChange' }, @@ -627,13 +874,17 @@ kiwi.view.ControlBox = Backbone.View.extend({ // Hold tab autocomplete data this.tabcomplete = {active: false, data: [], prefix: ''}; - kiwi.gateway.bind('change:nick', function () { + _kiwi.gateway.bind('change:nick', function () { $('.nick', that.$el).text(this.get('nick')); }); + + _kiwi.app.connections.on('active', function(panel, connection) { + $('.nick', that.$el).text(connection.get('nick')); + }); }, showNickChange: function (ev) { - (new kiwi.view.NickChangeBox()).render(); + (new _kiwi.view.NickChangeBox()).render(); }, process: function (ev) { @@ -643,7 +894,7 @@ kiwi.view.ControlBox = Backbone.View.extend({ meta; if (navigator.appVersion.indexOf("Mac") !== -1) { - meta = ev.ctrlKey; + meta = ev.metaKey; } else { meta = ev.altKey; } @@ -660,13 +911,16 @@ kiwi.view.ControlBox = Backbone.View.extend({ inp_val = inp_val.trim(); if (inp_val) { - this.processInput(inp_val); + $.each(inp_val.split('\n'), function (idx, line) { + that.processInput(line); + }); this.buffer.push(inp_val); this.buffer_pos = this.buffer.length; } inp.val(''); + return false; break; @@ -684,12 +938,12 @@ kiwi.view.ControlBox = Backbone.View.extend({ } break; - case (ev.keyCode === 37 && meta): // left - kiwi.app.panels.view.prev(); + case (ev.keyCode === 219 && meta): // [ + meta + _kiwi.app.panels.view.prev(); return false; - case (ev.keyCode === 39 && meta): // right - kiwi.app.panels.view.next(); + case (ev.keyCode === 221 && meta): // ] + meta + _kiwi.app.panels.view.next(); return false; case (ev.keyCode === 9): // tab @@ -697,7 +951,7 @@ kiwi.view.ControlBox = Backbone.View.extend({ if (_.isEqual(this.tabcomplete.data, [])) { // Get possible autocompletions var ac_data = []; - $.each(kiwi.app.panels.active.get('members').models, function (i, member) { + $.each(_kiwi.app.panels.active.get('members').models, function (i, member) { if (!member) return; ac_data.push(member.get('nick')); }); @@ -712,12 +966,20 @@ kiwi.view.ControlBox = Backbone.View.extend({ } (function () { - var tokens = inp_val.substring(0, inp[0].selectionStart).split(' '), - val, - p1, - newnick, - range, - nick = tokens[tokens.length - 1]; + var tokens, // Words before the cursor position + val, // New value being built up + p1, // Position in the value just before the nick + newnick, // New nick to be displayed (cycles through) + range, // TextRange for setting new text cursor position + nick, // Current nick in the value + trailing = ': '; // Text to be inserted after a tabbed nick + + tokens = inp_val.substring(0, inp[0].selectionStart).split(' '); + if (tokens[tokens.length-1] == ':') + tokens.pop(); + + nick = tokens[tokens.length - 1]; + if (this.tabcomplete.prefix === '') { this.tabcomplete.prefix = nick; } @@ -727,21 +989,31 @@ kiwi.view.ControlBox = Backbone.View.extend({ }); if (this.tabcomplete.data.length > 0) { + // Get the current value before cursor position p1 = inp[0].selectionStart - (nick.length); val = inp_val.substr(0, p1); + + // Include the current selected nick newnick = this.tabcomplete.data.shift(); this.tabcomplete.data.push(newnick); val += newnick; + + if (inp_val.substr(inp[0].selectionStart, 2) !== trailing) + val += trailing; + + // Now include the rest of the current value val += inp_val.substr(inp[0].selectionStart); + inp.val(val); + // Move the cursor position to the end of the nick if (inp[0].setSelectionRange) { - inp[0].setSelectionRange(p1 + newnick.length, p1 + newnick.length); + inp[0].setSelectionRange(p1 + newnick.length + trailing.length, p1 + newnick.length + trailing.length); } else if (inp[0].createTextRange) { // not sure if this bit is actually needed.... range = inp[0].createTextRange(); range.collapse(true); - range.moveEnd('character', p1 + newnick.length); - range.moveStart('character', p1 + newnick.length); + range.moveEnd('character', p1 + newnick.length + trailing.length); + range.moveStart('character', p1 + newnick.length + trailing.length); range.select(); } } @@ -756,13 +1028,17 @@ kiwi.view.ControlBox = Backbone.View.extend({ pre_processed; // The default command - if (command_raw[0] !== '/') { - command_raw = '/msg ' + kiwi.app.panels.active.get('name') + ' ' + command_raw; + if (command_raw[0] !== '/' || command_raw.substr(0, 2) === '//') { + // Remove any slash escaping at the start (ie. //) + command_raw = command_raw.replace(/^\/\//, '/'); + + // Prepend the default command + command_raw = '/msg ' + _kiwi.app.panels.active.get('name') + ' ' + command_raw; } // Process the raw command for any aliases - this.preprocessor.vars.server = kiwi.gateway.get('name'); - this.preprocessor.vars.channel = kiwi.app.panels.active.get('name'); + this.preprocessor.vars.server = _kiwi.gateway.get('name'); + this.preprocessor.vars.channel = _kiwi.app.panels.active.get('name'); this.preprocessor.vars.destination = this.preprocessor.vars.channel; command_raw = this.preprocessor.process(command_raw); @@ -774,25 +1050,32 @@ kiwi.view.ControlBox = Backbone.View.extend({ } else { // Default command command = 'msg'; - params.unshift(kiwi.app.panels.active.get('name')); + params.unshift(_kiwi.app.panels.active.get('name')); } // Trigger the command events this.trigger('command', {command: command, params: params}); - this.trigger('command_' + command, {command: command, params: params}); + this.trigger('command:' + command, {command: command, params: params}); // If we didn't have any listeners for this event, fire a special case // TODO: This feels dirty. Should this really be done..? - if (!this._callbacks['command_' + command]) { + if (!this._callbacks['command:' + command]) { this.trigger('unknown_command', {command: command, params: params}); } + }, + + + addPluginIcon: function ($icon) { + var $tool = $('
    ').append($icon); + this.$el.find('.input_tools').append($tool); + _kiwi.app.view.doLayout(); } }); -kiwi.view.StatusMessage = Backbone.View.extend({ +_kiwi.view.StatusMessage = Backbone.View.extend({ initialize: function () { this.$el.hide(); @@ -807,7 +1090,7 @@ kiwi.view.StatusMessage = Backbone.View.extend({ opt.timeout = opt.timeout || 5000; this.$el.text(text).attr('class', opt.type); - this.$el.slideDown(kiwi.app.view.doLayout); + this.$el.slideDown($.proxy(_kiwi.app.view.doLayout, this)); if (opt.timeout) this.doTimeout(opt.timeout); }, @@ -819,13 +1102,13 @@ kiwi.view.StatusMessage = Backbone.View.extend({ opt.timeout = opt.timeout || 5000; this.$el.html(text).attr('class', opt.type); - this.$el.slideDown(kiwi.app.view.doLayout); + this.$el.slideDown(_kiwi.app.view.doLayout); if (opt.timeout) this.doTimeout(opt.timeout); }, hide: function () { - this.$el.slideUp(kiwi.app.view.doLayout); + this.$el.slideUp($.proxy(_kiwi.app.view.doLayout, this)); }, doTimeout: function (length) { @@ -838,7 +1121,7 @@ kiwi.view.StatusMessage = Backbone.View.extend({ -kiwi.view.ResizeHandler = Backbone.View.extend({ +_kiwi.view.ResizeHandler = Backbone.View.extend({ events: { 'mousedown': 'startDrag', 'mouseup': 'stopDrag' @@ -864,34 +1147,44 @@ kiwi.view.ResizeHandler = Backbone.View.extend({ this.$el.css('left', event.clientX - (this.$el.outerWidth(true) / 2)); $('#memberlists').css('width', this.$el.parent().width() - (this.$el.position().left + this.$el.outerWidth())); - kiwi.app.view.doLayout(); + _kiwi.app.view.doLayout(); } }); -kiwi.view.AppToolbar = Backbone.View.extend({ +_kiwi.view.AppToolbar = Backbone.View.extend({ events: { 'click .settings': 'clickSettings' }, initialize: function () { - console.log('apptoolbar created', this.$el); }, clickSettings: function (event) { - console.log('clicked'); - kiwi.app.controlbox.processInput('/settings'); + _kiwi.app.controlbox.processInput('/settings'); } }); -kiwi.view.Application = Backbone.View.extend({ +_kiwi.view.Application = Backbone.View.extend({ initialize: function () { - $(window).resize(this.doLayout); - $('#toolbar').resize(this.doLayout); - $('#controlbox').resize(this.doLayout); + var that = this; + + $(window).resize(function() { that.doLayout.apply(that); }); + $('#toolbar').resize(function() { that.doLayout.apply(that); }); + $('#controlbox').resize(function() { that.doLayout.apply(that); }); + + // Change the theme when the config is changed + _kiwi.global.settings.on('change:theme', this.updateTheme, this); + this.updateTheme(getQueryVariable('theme')); + + _kiwi.global.settings.on('change:channel_list_style', this.setTabLayout, this); + this.setTabLayout(_kiwi.global.settings.get('channel_list_style')); + + _kiwi.global.settings.on('change:show_timestamps', this.displayTimestamps, this); + this.displayTimestamps(_kiwi.global.settings.get('show_timestamps')); this.doLayout(); @@ -899,22 +1192,74 @@ kiwi.view.Application = Backbone.View.extend({ // Confirmation require to leave the page window.onbeforeunload = function () { - if (kiwi.gateway.isConnected()) { + if (_kiwi.gateway.isConnected()) { return 'This will close all KiwiIRC conversations. Are you sure you want to close this window?'; } }; + + this.initSound(); + }, + + + + updateTheme: function (theme_name) { + // If called by the settings callback, get the correct new_value + if (theme_name === _kiwi.global.settings) { + theme_name = arguments[1]; + } + + // If we have no theme specified, get it from the settings + if (!theme_name) theme_name = _kiwi.global.settings.get('theme'); + + // Clear any current theme + this.$el.removeClass(function (i, css) { + return (css.match(/\btheme_\S+/g) || []).join(' '); + }); + + // Apply the new theme + this.$el.addClass('theme_' + (theme_name || 'relaxed')); + }, + + + setTabLayout: function (layout_style) { + // If called by the settings callback, get the correct new_value + if (layout_style === _kiwi.global.settings) { + layout_style = arguments[1]; + } + + if (layout_style == 'list') { + this.$el.addClass('chanlist_treeview'); + } else { + this.$el.removeClass('chanlist_treeview'); + } + + this.doLayout(); + }, + + + displayTimestamps: function (show_timestamps) { + // If called by the settings callback, get the correct new_value + if (show_timestamps === _kiwi.global.settings) { + show_timestamps = arguments[1]; + } + + if (show_timestamps) { + this.$el.addClass('timestamps'); + } else { + this.$el.removeClass('timestamps'); + } }, // Globally shift focus to the command input box on a keypress setKeyFocus: function (ev) { // If we're copying text, don't shift focus - if (ev.ctrlKey || ev.altKey) { + if (ev.ctrlKey || ev.altKey || ev.metaKey) { return; } // If we're typing into an input box somewhere, ignore - if ((ev.target.tagName.toLowerCase() === 'input') || (ev.target.id === 'edittopic')) { + if ((ev.target.tagName.toLowerCase() === 'input') || (ev.target.tagName.toLowerCase() === 'textarea') || $(ev.target).attr('contenteditable')) { return; } @@ -923,30 +1268,60 @@ kiwi.view.Application = Backbone.View.extend({ doLayout: function () { - var el_panels = $('#panels'); - var el_memberlists = $('#memberlists'); - var el_toolbar = $('#toolbar'); - var el_controlbox = $('#controlbox'); - var el_resize_handle = $('#memberlists_resize_handle'); + var el_kiwi = this.$el; + var el_panels = $('#kiwi #panels'); + var el_memberlists = $('#kiwi #memberlists'); + var el_toolbar = $('#kiwi #toolbar'); + var el_controlbox = $('#kiwi #controlbox'); + var el_resize_handle = $('#kiwi #memberlists_resize_handle'); var css_heights = { top: el_toolbar.outerHeight(true), bottom: el_controlbox.outerHeight(true) }; + + // If any elements are not visible, full size the panals instead + if (!el_toolbar.is(':visible')) { + css_heights.top = 0; + } + + if (!el_controlbox.is(':visible')) { + css_heights.bottom = 0; + } + + // Apply the CSS sizes el_panels.css(css_heights); el_memberlists.css(css_heights); el_resize_handle.css(css_heights); + // If we have channel tabs on the side, adjust the height + if (el_kiwi.hasClass('chanlist_treeview')) { + $('#tabs', el_kiwi).css(css_heights); + } + + // Determine if we have a narrow window (mobile/tablet/or even small desktop window) + if (el_kiwi.outerWidth() < 400) { + el_kiwi.addClass('narrow'); + } else { + el_kiwi.removeClass('narrow'); + } + + // Set the panels width depending on the memberlist visibility if (el_memberlists.css('display') != 'none') { - // Handle + panels to the side of the memberlist - el_panels.css('right', el_memberlists.outerWidth(true) + el_resize_handle.outerWidth(true)); - el_resize_handle.css('left', el_memberlists.position().left - el_resize_handle.outerWidth(true)); + // Panels to the side of the memberlist + el_panels.css('right', el_memberlists.outerWidth(true)); + // The resize handle sits overlapping the panels and memberlist + el_resize_handle.css('left', el_memberlists.position().left - (el_resize_handle.outerWidth(true) / 2)); } else { - // Memberlist is hidden so handle + panels to the right edge - el_panels.css('right', el_resize_handle.outerWidth(true)); + // Memberlist is hidden so panels to the right edge + el_panels.css('right', 0); + // And move the handle just out of sight to the right el_resize_handle.css('left', el_panels.outerWidth(true)); } + + var input_wrap_width = parseInt($('#kiwi #controlbox .input_tools').outerWidth()); + el_controlbox.find('.input_wrap').css('right', input_wrap_width + 7); }, @@ -1025,8 +1400,8 @@ kiwi.view.Application = Backbone.View.extend({ var that = this; if (!instant) { - $('#toolbar').slideUp({queue: false, duration: 400, step: this.doLayout}); - $('#controlbox').slideUp({queue: false, duration: 400, step: this.doLayout}); + $('#toolbar').slideUp({queue: false, duration: 400, step: $.proxy(this.doLayout, this)}); + $('#controlbox').slideUp({queue: false, duration: 400, step: $.proxy(this.doLayout, this)}); } else { $('#toolbar').slideUp(0); $('#controlbox').slideUp(0); @@ -1038,12 +1413,267 @@ kiwi.view.Application = Backbone.View.extend({ var that = this; if (!instant) { - $('#toolbar').slideDown({queue: false, duration: 400, step: this.doLayout}); - $('#controlbox').slideDown({queue: false, duration: 400, step: this.doLayout}); + $('#toolbar').slideDown({queue: false, duration: 400, step: $.proxy(this.doLayout, this)}); + $('#controlbox').slideDown({queue: false, duration: 400, step: $.proxy(this.doLayout, this)}); } else { $('#toolbar').slideDown(0); $('#controlbox').slideDown(0); this.doLayout(); } + }, + + + initSound: function () { + var that = this, + base_path = this.model.get('base_path'); + + $script(base_path + '/assets/libs/soundmanager2/soundmanager2-nodebug-jsmin.js', function() { + if (typeof soundManager === 'undefined') + return; + + soundManager.setup({ + url: base_path + '/assets/libs/soundmanager2/', + flashVersion: 9, // optional: shiny features (default = 8)// optional: ignore Flash where possible, use 100% HTML5 mode + preferFlash: true, + + onready: function() { + that.sound_object = soundManager.createSound({ + id: 'highlight', + url: base_path + '/assets/sound/highlight.mp3' + }); + } + }); + }); + }, + + + playSound: function (sound_id) { + if (!this.sound_object) return; + + if (_kiwi.global.settings.get('mute_sounds')) + return; + + soundManager.play(sound_id); + } +}); + + + + + + + + + +_kiwi.view.MediaMessage = Backbone.View.extend({ + events: { + 'click .media_close': 'close' + }, + + initialize: function () { + // Get the URL from the data + this.url = this.$el.data('url'); + }, + + // Close the media content and remove it from display + close: function () { + var that = this; + this.$content.slideUp('fast', function () { + that.$content.remove(); + }); + }, + + // Open the media content within its wrapper + open: function () { + // Create the content div if we haven't already + if (!this.$content) { + this.$content = $('
    Close media
    '); + this.$content.find('.content').append(this.mediaTypes[this.$el.data('type')].apply(this, []) || 'Not found :('); + } + + // Now show the content if not already + if (!this.$content.is(':visible')) { + // Hide it first so the slideDown always plays + this.$content.hide(); + + // Add the media content and slide it into view + this.$el.append(this.$content); + this.$content.slideDown(); + } + }, + + + + // Generate the media content for each recognised type + mediaTypes: { + twitter: function () { + var tweet_id = this.$el.data('tweetid'); + var that = this; + + $.getJSON('https://api.twitter.com/1/statuses/oembed.json?id=' + tweet_id + '&callback=?', function (data) { + that.$content.find('.content').html(data.html); + }); + + return $('
    Loading tweet..
    '); + }, + + + image: function () { + return $(''); + }, + + + reddit: function () { + var that = this; + var matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(this.url); + + $.getJSON('http://www.' + matches[0] + '.json?jsonp=?', function (data) { + console.log('Loaded reddit data', data); + var post = data[0].data.children[0].data; + var thumb = ''; + + // Show a thumbnail if there is one + if (post.thumbnail) { + //post.thumbnail = 'http://www.eurotunnel.com/uploadedImages/commercial/back-steps-icon-arrow.png'; + + // Hide the thumbnail if an over_18 image + if (post.over_18) { + thumb = ''; + thumb += '

    Show
    NSFW

    '; + thumb += ''; + thumb += '
    '; + } else { + thumb = ''; + } + } + + // Build the template string up + var tmpl = '
    ' + thumb + '<%- title %>
    Posted by <%- author %>.    '; + tmpl += ' <%- ups %>    <%- downs %>
    '; + tmpl += '<%- num_comments %> comments made. View post
    '; + + that.$content.find('.content').html(_.template(tmpl, post)); + }); + + return $('
    Loading Reddit thread..
    '); + } + } + +}, { + + // Build the closed media HTML from a URL + buildHtml: function (url) { + var html = '', matches; + + // Is it an image? + if (url.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)) { + html += ''; + } + + // Is it a tweet? + matches = (/https?:\/\/twitter.com\/([a-zA-Z0-9_]+)\/status\/([0-9]+)/ig).exec(url); + if (matches) { + html += ''; + } + + // Is reddit? + matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(url); + if (matches) { + html += ''; + } + + return html; + } +}); + + + +_kiwi.view.MenuBox = Backbone.View.extend({ + events: { + 'click .ui_menu_foot .close': 'dispose' + }, + + initialize: function(title) { + var that = this; + + this.$el = $('
    '); + + this._title = title || ''; + this._items = {}; + this._display_footer = true; + + this._close_proxy = function(event) { + that.onDocumentClick(event); + }; + $(document).on('click', this._close_proxy); + }, + + + render: function() { + var that = this; + + this.$el.find('*').remove(); + + if (this._title) { + $('
    ') + .text(this._title) + .appendTo(this.$el); + } + + + _.each(this._items, function(item) { + var $item = $('
    ') + .append(item); + + that.$el.append($item); + }); + + if (this._display_footer) + this.$el.append('
    Close
    '); + }, + + + onDocumentClick: function(event) { + var $target = $(event.target); + + // If this is not itself AND we don't contain this element, dispose $el + if ($target[0] != this.$el[0] && this.$el.has($target).length === 0) + this.dispose(); + }, + + + dispose: function() { + _.each(this._items, function(item) { + item.dispose && item.dispose(); + item.remove && item.remove(); + }); + + this._items = null; + this.remove(); + + $(document).off('click', this._close_proxy); + }, + + + addItem: function(item_name, $item) { + $item = $($item); + if ($item.is('a')) $item.addClass('icon-chevron-right'); + this._items[item_name] = $item; + }, + + + removeItem: function(item_name) { + delete this._items[item_name]; + }, + + + showFooter: function(show) { + this._show_footer = show; + }, + + + show: function() { + this.render(); + this.$el.appendTo(_kiwi.app.view.$el); } -}); \ No newline at end of file +});