From 72db27e431fe02f2cc4ed2be05c757660b97884a Mon Sep 17 00:00:00 2001 From: Darren Date: Wed, 1 Jan 2014 16:59:29 +0000 Subject: [PATCH] Channel Info+admin window; RPL_CHANNEL_URL; Ban list; CSS forms refactor --- client/assets/css/style.css | 117 +++++++-------- client/src/app.js | 3 +- client/src/index.html.tmpl | 246 +++++++++++++++++++------------ client/src/models/application.js | 11 ++ client/src/models/channel.js | 5 + client/src/models/channelinfo.js | 5 + client/src/models/gateway.js | 29 ++++ client/src/models/network.js | 32 +++- client/src/views/channelinfo.js | 153 +++++++++++++++++++ client/src/views/menubox.js | 1 - server/clientcommands.js | 9 +- server/httphandler.js | 6 +- server/irc/channel.js | 18 ++- server/irc/commands.js | 138 +++++++++++------ 14 files changed, 554 insertions(+), 219 deletions(-) create mode 100644 client/src/models/channelinfo.js create mode 100644 client/src/views/channelinfo.js diff --git a/client/assets/css/style.css b/client/assets/css/style.css index 4afa2f8..a6fe3e2 100644 --- a/client/assets/css/style.css +++ b/client/assets/css/style.css @@ -14,6 +14,25 @@ html, body { height:100%; } #kiwi a img { border:none; } #kiwi .format_span a { color: inherit; background-color: inherit; text-decoration: inherit; font-style: inherit; font-weight: inherit;} +#kiwi h1, +#kiwi h2, +#kiwi h3 { + margin-top: 22px; + margin-bottom: 11px; +} +#kiwi h4, +#kiwi h5, +#kiwi h6 { + margin-top: 11px; + margin-bottom: 11px; +} +#kiwi h1 { font-size: 46px; } +#kiwi h2 { font-size: 40px; } +#kiwi h3 { font-size: 34px; } +#kiwi h4 { font-size: 28px; } +#kiwi h5 { font-size: 22px; } +#kiwi h6 { font-size: 16px; } + /** * Main layout blocks @@ -271,97 +290,63 @@ html, body { height:100%; } display: inline-block; } -#kiwi .settings_container h1 { - margin-top: 22px; - margin-bottom: 11px; - font-size: 46px; -} - -#kiwi .settings_container h2 { - margin-top: 22px; - margin-bottom: 11px; - font-size: 40px; -} - -#kiwi .settings_container h3 { - margin-top: 22px; - margin-bottom: 11px; - font-size: 34px; -} - -#kiwi .settings_container h4 { - margin-top: 11px; - margin-bottom: 11px; - font-size: 28px; -} - -#kiwi .settings_container h5 { - margin-top: 11px; - margin-bottom: 11px; - font-size: 22px; -} - -#kiwi .settings_container h6 { - margin-top: 11px; - margin-bottom: 11px; - font-size: 16px; -} - #kiwi .settings_container label { cursor: pointer; } -#kiwi .settings_container input, -#kiwi .settings_container select, -#kiwi .settings_container textarea { - width: 100%; +#kiwi_ form label { display: block; } +#kiwi form input, +#kiwi form select, +#kiwi form textarea { + /*width: 100%; + box-sizing: border-box;*/ } -#kiwi .settings_container input[size], -#kiwi .settings_container select[size], -#kiwi .settings_container textarea[size] { +#kiwi form input[size], +#kiwi form select[size], +#kiwi form textarea[size] { width: auto; } -#kiwi .settings_container input[type="file"], -#kiwi .settings_container input[type="image"], -#kiwi .settings_container input[type="submit"], -#kiwi .settings_container input[type="reset"], -#kiwi .settings_container input[type="button"] { +#kiwi form input[type="file"], +#kiwi form input[type="image"], +#kiwi form input[type="submit"], +#kiwi form input[type="reset"], +#kiwi form input[type="button"] { width: auto; } -#kiwi .settings_container input[type="radio"] { +#kiwi form input[type="radio"] { width: auto; cursor: pointer; margin-top: 2px; } -#kiwi .settings_container input[type="checkbox"] { +#kiwi form input[type="checkbox"] { width: auto; cursor: pointer; margin-top: 3px; } -#kiwi .settings_container .radio, -#kiwi .settings_container .checkbox { +#kiwi form .radio, +#kiwi form .checkbox { margin-bottom: 10px; padding-left: 20px; } -#kiwi .settings_container .radio:last-child, -#kiwi .settings_container .checkbox:last-child { +#kiwi form .radio:last-child, +#kiwi form .checkbox:last-child { margin-bottom: 0; } -#kiwi .settings_container .radio input[type="radio"], -#kiwi .settings_container .checkbox input[type="checkbox"] { +#kiwi form .radio input[type="radio"], +#kiwi form .checkbox input[type="checkbox"] { float: left; margin-left: -20px; } -#kiwi .settings_container .radio+.radio, -#kiwi .settings_container .checkbox+.checkbox { +#kiwi form .radio+.radio, +#kiwi form .checkbox+.checkbox { margin-top: -7px; } @@ -397,11 +382,11 @@ html, body { height:100%; } width: 75px; } -#kiwi .settings_container .control-group { +#kiwi .control-group { margin: 0 0 20px 20px; } -#kiwi .settings_container .control-group:last-child { +#kiwi .control-group:last-child { margin-bottom: 0; } @@ -414,6 +399,14 @@ html, body { height:100%; } } +#kiwi .schannel_info {} +#kiwi .channel_info label { display: block; } +#kiwi .channel_info .channel_url { + display: none; +} +#kiwi .channel_info .remove-ban { cursor: pointer; } + + @@ -746,7 +739,7 @@ html, body { height:100%; } padding:10px; left: 0px; background: #1B1B1B; color:#eeeeee; } -#kiwi.theme_relaxed .controlbox .nickchange input { padding:0.3em 0.5em; margin-left: 0.5em; } +#kiwi.theme_relaxed .controlbox .nickchange input { padding:0.3em 0.5em; margin-left: 0.5em; width: 165px; } #kiwi.theme_relaxed .controlbox .nickchange button { padding:0.5em; margin: 0 0.5em 0 1em; } @@ -964,7 +957,7 @@ html, body { height:100%; } #kiwi.theme_mini .messages .msg { border-bottom: 1px solid #DEDEDE; padding: 5px; font-family:arial; font-size:0.9em; } #kiwi.theme_mini .messages .msg .time { display:none; } -#kiwi.theme_mini .messages .msg .nick { display:block; font-family:Arial; text-transform:capitalize; } +#kiwi.theme_mini .messages .msg .nick { display:block; font-family:Arial; text-tranform:capitalize; } #kiwi.theme_mini .messages .msg .text { display:block; white-space:pre-wrap; word-wrap:break-word; font-family:arial; } #kiwi.theme_mini .messages .msg.action .nick { } diff --git a/client/src/app.js b/client/src/app.js index 885684b..e5790ce 100644 --- a/client/src/app.js +++ b/client/src/app.js @@ -65,7 +65,8 @@ _kiwi.global = { var funcs = { kiwi: 'kiwi', raw: 'raw', kick: 'kick', topic: 'topic', part: 'part', join: 'join', action: 'action', ctcp: 'ctcp', - notice: 'notice', msg: 'privmsg', changeNick: 'changeNick' + notice: 'notice', msg: 'privmsg', changeNick: 'changeNick', + channelInfo: 'channelInfo', mode: 'mode' }; // Proxy each gateway method diff --git a/client/src/index.html.tmpl b/client/src/index.html.tmpl index aeed33d..9872268 100644 --- a/client/src/index.html.tmpl +++ b/client/src/index.html.tmpl @@ -55,6 +55,57 @@ + + + diff --git a/client/src/models/application.js b/client/src/models/application.js index e65c2ca..80a2a85 100644 --- a/client/src/models/application.js +++ b/client/src/models/application.js @@ -495,6 +495,8 @@ _kiwi.model.Application = function () { '/voice': '/quote mode $channel +v $1+', '/devoice': '/quote mode $channel -v $1+', '/k': '/kick $channel $1+', + '/ban': '/quote mode $channel +b $1+', + '/unban': '/quote mode $channel -b $1+', // Misc aliases '/slap': '/me slaps $1 around a bit with a large trout' @@ -539,6 +541,15 @@ _kiwi.model.Application = function () { controlbox.on('command:encoding', encodingCommand); + controlbox.on('command:info', function(ev) { + var active_panel = _kiwi.app.panels().active; + + if (!active_panel.isChannel()) + return; + + new _kiwi.model.ChannelInfo({channel: _kiwi.app.panels().active}); + }); + controlbox.on('command:css', function (ev) { var queryString = '?reload=' + new Date().getTime(); $('link[rel="stylesheet"]').each(function () { diff --git a/client/src/models/channel.js b/client/src/models/channel.js index 7fb124a..18d0538 100644 --- a/client/src/models/channel.js +++ b/client/src/models/channel.js @@ -105,5 +105,10 @@ _kiwi.model.Channel = _kiwi.model.Panel.extend({ this.addMsg('', 'Window cleared'); this.view.render(); + }, + + + setMode: function(mode_string) { + this.get('network').gateway.mode(this.get('name'), mode_string); } }); diff --git a/client/src/models/channelinfo.js b/client/src/models/channelinfo.js new file mode 100644 index 0000000..501c9fa --- /dev/null +++ b/client/src/models/channelinfo.js @@ -0,0 +1,5 @@ +_kiwi.model.ChannelInfo = Backbone.Model.extend({ + initialize: function () { + this.view = new _kiwi.view.ChannelInfo({"model": this}); + } +}); \ No newline at end of file diff --git a/client/src/models/gateway.js b/client/src/models/gateway.js index 0a010b7..268b634 100644 --- a/client/src/models/gateway.js +++ b/client/src/models/gateway.js @@ -465,6 +465,20 @@ _kiwi.model.Gateway = function () { this.sendData(connection_id, data, callback); }; + /** + * Retrieves channel information + */ + this.channelInfo = function (connection_id, channel, callback) { + var data = { + method: 'channel_info', + args: { + channel: channel + } + }; + + this.sendData(connection_id, data, callback); + }; + /** * Leaves a channel * @param {String} channel The channel to part @@ -568,6 +582,21 @@ _kiwi.model.Gateway = function () { this.sendData(connection_id, data, callback); }; + /** + * Sets a mode for a target + */ + this.mode = function (connection_id, target, mode_string, callback) { + data = { + method: 'raw', + args: { + data: 'MODE ' + target + ' ' + mode_string + } + }; + + this.sendData(connection_id, data, callback); + }; + + /** * Sends ENCODING change request to server. * @param {String} new_encoding The new proposed encode diff --git a/client/src/models/network.js b/client/src/models/network.js index 61ed241..a80445b 100644 --- a/client/src/models/network.js +++ b/client/src/models/network.js @@ -127,6 +127,7 @@ this.gateway.on('topicsetby', onTopicSetBy, this); this.gateway.on('userlist', onUserlist, this); this.gateway.on('userlist_end', onUserlistEnd, this); + this.gateway.on('banlist', onBanlist, this); this.gateway.on('mode', onMode, this); this.gateway.on('whois', onWhois, this); this.gateway.on('whowas', onWhowas, this); @@ -134,6 +135,7 @@ this.gateway.on('list_start', onListStart, this); this.gateway.on('irc_error', onIrcError, this); this.gateway.on('unknown_command', onUnknownCommand, this); + this.gateway.on('channel_info', onChannelInfo, this); }, @@ -170,7 +172,7 @@ // Check if we have the panel already. If not, create it channel = that.panels.getByName(channel_name); if (!channel) { - channel = new _kiwi.model.Channel({name: channel_name}); + channel = new _kiwi.model.Channel({name: channel_name, network: that}); that.panels.add(channel); } @@ -264,7 +266,7 @@ var c, members, user; c = this.panels.getByName(event.channel); if (!c) { - c = new _kiwi.model.Channel({name: event.channel}); + c = new _kiwi.model.Channel({name: event.channel, network: this}); this.panels.add(c); } @@ -481,7 +483,7 @@ // If a panel isn't found for this PM, create one panel = this.panels.getByName(event.nick); if (!panel) { - panel = new _kiwi.model.Channel({name: event.nick}); + panel = new _kiwi.model.Channel({name: event.nick, network: this}); this.panels.add(panel); } @@ -526,6 +528,19 @@ + function onChannelInfo(event) { + var channel = this.panels.getByName(event.channel); + if (!channel) return; + + if (event.url) { + channel.set('info_url', event.url); + } else if (event.modes) { + channel.set('info_modes', event.modes); + } + } + + + function onUserlist(event) { var channel; channel = this.panels.getByName(event.channel); @@ -558,6 +573,17 @@ + function onBanlist(event) { + console.log('banlist', event); + var channel = this.panels.getByName(event.channel); + if (!channel) + return; + + channel.set('banlist', event.bans || []); + } + + + function onMode(event) { var channel, i, prefixes, members, member, find_prefix; diff --git a/client/src/views/channelinfo.js b/client/src/views/channelinfo.js new file mode 100644 index 0000000..97dfde3 --- /dev/null +++ b/client/src/views/channelinfo.js @@ -0,0 +1,153 @@ +// var f = new _kiwi.model.ChannelInfo({channel: _kiwi.app.panels().active}); + +_kiwi.view.ChannelInfo = Backbone.View.extend({ + events: { + 'click .show_banlist': 'updateBanlist', + 'change .channel-mode': 'onModeChange', + 'click .remove-ban': 'onRemoveBanClick' + }, + + + initialize: function () { + var that = this, + network, + channel = this.model.get('channel'), + text = { + channel_name: channel.get('name') + }; + + this.$el = $(_.template($('#tmpl_channel_info').html().trim(), text)); + + // Create the menu box this view will sit inside + this.menu = new _kiwi.view.MenuBox(); + this.menu.addItem('channel_info', this.$el); + this.menu.$el.appendTo(channel.view.$container); + this.menu.show(); + + // Menu box will call this destroy on closing + this.$el.dispose = _.bind(this.dispose, this); + + // Display the info we have, then listen for further changes + this.updateInfo(channel); + channel.on('change:info_modes change:info_url change:banlist', this.updateInfo, this); + + // Request the latest info for ths channel from the network + channel.get('network').gateway.channelInfo(channel.get('name')); + }, + + + render: function () { + }, + + + onModeChange: function(event) { + var $this = $(event.currentTarget), + channel = this.model.get('channel'), + mode = $this.data('mode'), + mode_string = ''; + + if ($this.attr('type') == 'checkbox') { + mode_string = $this.is(':checked') ? '+' : '-'; + mode_string += mode; + channel.setMode(mode_string); + + return; + } + + if ($this.attr('type') == 'text') { + mode_string = $this.val() ? + '+' + mode + ' ' + $this.val() : + '-' + mode; + + channel.setMode(mode_string); + + return; + } + }, + + + onRemoveBanClick: function (event) { + event.preventDefault(); + event.stopPropagation(); + + var $this = $(event.currentTarget), + $tr = $this.parents('tr:first'), + ban = $tr.data('ban'); + + if (!ban) + return; + + var channel = this.model.get('channel'); + channel.setMode('-b ' + ban.banned); + + $tr.remove(); + }, + + + updateInfo: function (channel, new_val) { + var that = this, + modes, url, banlist; + + modes = channel.get('info_modes'); + if (modes) { + _.each(modes, function(mode, idx) { + mode.mode = mode.mode.toLowerCase(); + + if (mode.mode == '+k') { + that.$el.find('[name="channel_key"]').val(mode.param); + } else if (mode.mode == '+m') { + that.$el.find('[name="channel_mute"]').attr('checked', 'checked'); + } else if (mode.mode == '+i') { + that.$el.find('[name="channel_invite"]').attr('checked', 'checked'); + } else if (mode.mode == '+n') { + that.$el.find('[name="channel_external_messages"]').attr('checked', 'checked'); + } else if (mode.mode == '+t') { + that.$el.find('[name="channel_topic"]').attr('checked', 'checked'); + } + }); + } + + url = channel.get('info_url'); + if (url) { + this.$el.find('.channel_url a') + .text(url) + .attr('href', url); + + this.$el.find('.channel_url').slideDown(); + } + + banlist = channel.get('banlist'); + if (banlist && banlist.length) { + var $table = this.$el.find('.channel_banlist table tbody'); + + $table.empty(); + _.each(banlist, function(ban) { + var $tr = $('').data('ban', ban); + + $('').text(ban.banned).appendTo($tr); + $('').text(ban.banned_by.split(/[!@]/)[0]).appendTo($tr); + $('').text(formatDate(new Date(parseInt(ban.banned_at, 10) * 1000))).appendTo($tr); + $('').appendTo($tr); + + $table.append($tr); + }); + } + }, + + + updateBanlist: function (event) { + event.preventDefault(); + + var channel = this.model.get('channel'), + network = channel.get('network'); + + network.gateway.raw('MODE ' + channel.get('name') + ' +b'); + }, + + + dispose: function () { + this.model.get('channel').off('change:info_modes change:info_url change:banlist', this.updateInfo, this); + + this.$el.remove(); + } +}); \ No newline at end of file diff --git a/client/src/views/menubox.js b/client/src/views/menubox.js index 9084b74..71931c3 100644 --- a/client/src/views/menubox.js +++ b/client/src/views/menubox.js @@ -66,7 +66,6 @@ _kiwi.view.MenuBox = Backbone.View.extend({ addItem: function(item_name, $item) { - $item = $($item); if ($item.is('a')) $item.addClass('icon-chevron-right'); this._items[item_name] = $item; }, diff --git a/server/clientcommands.js b/server/clientcommands.js index 33edeff..d87e13c 100644 --- a/server/clientcommands.js +++ b/server/clientcommands.js @@ -55,7 +55,7 @@ var listeners = { irc_connection.write('PRIVMSG ' + args.target + ' :' + block, cb); }); }, - + CTCP: function (args, irc_connection, callback) { if ((args.target) && (args.type)) { @@ -84,6 +84,13 @@ var listeners = { }, + CHANNEL_INFO: function (args, irc_connection, callback) { + if (args.channel) { + irc_connection.write('MODE ' + args.channel, callback); + } + }, + + PART: function (args, irc_connection, callback) { if (args.channel) { _.each(args.channel.split(","), function (chan) { diff --git a/server/httphandler.js b/server/httphandler.js index ee8b7e1..225e184 100644 --- a/server/httphandler.js +++ b/server/httphandler.js @@ -235,7 +235,8 @@ function generateSettings(request, debug, callback) { 'src/models/panel.js', 'src/models/member.js', 'src/models/memberlist.js', - 'src/models/network.js' + 'src/models/network.js', + 'src/models/channelinfo.js' ], [ @@ -280,7 +281,8 @@ function generateSettings(request, debug, callback) { 'src/views/statusmessage.js', 'src/views/tabs.js', 'src/views/topicbar.js', - 'src/views/userbox.js' + 'src/views/userbox.js', + 'src/views/channelinfo.js' ] ]); } else { diff --git a/server/irc/channel.js b/server/irc/channel.js index 2c20736..7c3a56e 100644 --- a/server/irc/channel.js +++ b/server/irc/channel.js @@ -25,7 +25,8 @@ var IrcChannel = function(irc_connection, name) { banlist: onBanList, banlist_end: onBanListEnd, topicsetby: onTopicSetBy, - mode: onMode + mode: onMode, + info: onChannelInfo }; EventBinder.bindIrcEvents('channel ' + this.name, this.irc_events, this, irc_connection); }; @@ -181,15 +182,24 @@ function onTopic(event) { } +function onChannelInfo(event) { + // Channel info event may contain 1 of several types of info, + // including creation time, modes. So just pipe the event + // right through to the client + this.irc_connection.clientEvent('channel_info', event); +} + + function onBanList(event) { this.ban_list_buffer.push(event); } function onBanListEnd(event) { - var that = this; - this.ban_list_buffer.forEach(function (ban) { - that.irc_connection.clientEvent('banlist', ban); + this.irc_connection.clientEvent('banlist', { + channel: this.name, + bans: this.ban_list_buffer }); + this.ban_list_buffer = []; } diff --git a/server/irc/commands.js b/server/irc/commands.js index f951dfa..03899aa 100644 --- a/server/irc/commands.js +++ b/server/irc/commands.js @@ -31,6 +31,9 @@ irc_numerics = { '321': 'RPL_LISTSTART', '322': 'RPL_LIST', '323': 'RPL_LISTEND', + '324': 'RPL_CHANNELMODEIS', + '328': 'RPL_CHANNEL_URL', + '329': 'RPL_CREATIONTIME', '330': 'RPL_WHOISACCOUNT', '331': 'RPL_NOTOPIC', '332': 'RPL_TOPIC', @@ -295,6 +298,34 @@ handlers = { }); }, + 'RPL_CHANNELMODEIS': function (command) { + var channel = command.params[1], + modes = parseModeList.call(this, command.params[2], command.params.slice(3)); + + this.irc_connection.emit('channel ' + channel + ' info', { + channel: channel, + modes: modes + }); + }, + + 'RPL_CREATIONTIME': function (command) { + var channel = command.params[1]; + + this.irc_connection.emit('channel ' + channel + ' info', { + channel: channel, + created_at: parseInt(command.params[2], 10) + }); + }, + + 'RPL_CHANNEL_URL': function (command) { + var channel = command.params[1]; + + this.irc_connection.emit('channel ' + channel + ' info', { + channel: channel, + url: command.trailing + }); + }, + 'RPL_MOTD': function (command) { this.irc_connection.emit('server ' + this.irc_connection.irc_host.hostname + ' motd', { motd: command.trailing + '\n' @@ -543,58 +574,13 @@ handlers = { }, 'MODE': function (command) { - var chanmodes = this.irc_connection.options.CHANMODES || [], - prefixes = this.irc_connection.options.PREFIX || [], - always_param = (chanmodes[0] || '').concat((chanmodes[1] || '')), - modes = [], - has_param, i, j, add, event, time; + var modes = [], event, time; // Check if we have a server-time time = getServerTime.call(this, command); - prefixes = _.reduce(prefixes, function (list, prefix) { - list.push(prefix.mode); - return list; - }, []); - always_param = always_param.split('').concat(prefixes); - - has_param = function (mode, add) { - if (_.find(always_param, function (m) { - return m === mode; - })) { - return true; - } else if (add && _.find((chanmodes[2] || '').split(''), function (m) { - return m === mode; - })) { - return true; - } else { - return false; - } - }; - - if (!command.params[1]) { - command.params[1] = command.trailing; - } - - j = 0; - for (i = 0; i < command.params[1].length; i++) { - switch (command.params[1][i]) { - case '+': - add = true; - break; - case '-': - add = false; - break; - default: - if (has_param(command.params[1][i], add)) { - modes.push({mode: (add ? '+' : '-') + command.params[1][i], param: command.params[2 + j]}); - j++; - } else { - modes.push({mode: (add ? '+' : '-') + command.params[1][i], param: null}); - } - } - } - + // Get a JSON representation of the modes + modes = parseModeList.call(this, command.params[1] || command.trailing, command.params.slice(2)); event = (_.contains(this.irc_connection.options.CHANTYPES, command.params[0][0]) ? 'channel ' : 'user ') + command.params[0] + ' mode'; this.irc_connection.emit(event, { @@ -999,6 +985,62 @@ function genericNotice (command, msg, is_error) { } +/** + * Convert a mode string such as '+k pass', or '-i' to a readable + * format. + * [ { mode: '+k', param: 'pass' } ] + * [ { mode: '-i', param: null } ] + */ +function parseModeList(mode_string, mode_params) { + var chanmodes = this.irc_connection.options.CHANMODES || [], + prefixes = this.irc_connection.options.PREFIX || [], + always_param = (chanmodes[0] || '').concat((chanmodes[1] || '')), + modes = [], + has_param, i, j, add; + + prefixes = _.reduce(prefixes, function (list, prefix) { + list.push(prefix.mode); + return list; + }, []); + always_param = always_param.split('').concat(prefixes); + + has_param = function (mode, add) { + if (_.find(always_param, function (m) { + return m === mode; + })) { + return true; + } else if (add && _.find((chanmodes[2] || '').split(''), function (m) { + return m === mode; + })) { + return true; + } else { + return false; + } + }; + + j = 0; + for (i = 0; i < mode_string.length; i++) { + switch (mode_string[i]) { + case '+': + add = true; + break; + case '-': + add = false; + break; + default: + if (has_param(mode_string[i], add)) { + modes.push({mode: (add ? '+' : '-') + mode_string[i], param: mode_params[j]}); + j++; + } else { + modes.push({mode: (add ? '+' : '-') + mode_string[i], param: null}); + } + } + } + + return modes; +} + + function getServerTime(command) { var time; -- 2.25.1