From de6fa69774dd81eb408b46be4fdadb314d6b91af Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 28 Apr 2016 16:35:14 +0200 Subject: [PATCH] [ADD] mail: commands support Use the mention mechanism to autocomplete on commands (keyword /). A first command /help has been implemented. --- addons/mail/controllers/main.py | 1 + addons/mail/models/mail_channel.py | 55 +++++++++++++++++++ addons/mail/static/src/js/chat_manager.js | 38 ++++++++++--- addons/mail/static/src/js/chatter.js | 1 + addons/mail/static/src/js/client_action.js | 6 +- addons/mail/static/src/js/composer.js | 33 ++++++++++- .../static/src/js/extended_chat_window.js | 6 +- addons/mail/static/src/js/thread.js | 6 +- addons/mail/static/src/xml/composer.xml | 6 ++ 9 files changed, 135 insertions(+), 17 deletions(-) diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py index 3e763c72b39..e6e61ff231a 100644 --- a/addons/mail/controllers/main.py +++ b/addons/mail/controllers/main.py @@ -242,6 +242,7 @@ class MailController(http.Controller): 'needaction_inbox_counter': request.env['res.partner'].get_needaction_count(), 'starred_counter': request.env['res.partner'].get_starred_count(), 'channel_slots': request.env['mail.channel'].channel_fetch_slot(), + 'commands': request.env['mail.channel'].get_mention_commands(), 'mention_partner_suggestions': request.env['res.partner'].get_static_mention_suggestions(), 'shortcodes': request.env['mail.shortcode'].sudo().search_read([], ['shortcode_type', 'source', 'substitution', 'description']), 'menu_id': request.env['ir.model.data'].xmlid_to_res_id('mail.mail_channel_menu_root_chat'), diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 02607bfcbc3..fa4ba2e8f02 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -2,11 +2,13 @@ from email.utils import formataddr +import re import uuid from openerp import _, api, fields, models, modules, tools from openerp.exceptions import UserError from openerp.osv import expression +from openerp.tools import ormcache @@ -573,3 +575,56 @@ class Channel(models.Model): del(channel['message_id']) channel['last_message'] = message return channels_preview.values() + + #------------------------------------------------------ + # Commands + #------------------------------------------------------ + @api.model + @ormcache() + def get_mention_commands(self): + """ Returns the allowed commands in channels """ + commands = [] + for n in dir(self): + match = re.search('^_define_command_(.+?)$', n) + if match: + command = getattr(self, n)() + command['name'] = match.group(1) + commands.append(command) + return commands + + @api.multi + def execute_command(self, command='', **kwargs): + """ Executes a given command """ + self.ensure_one() + command_callback = getattr(self, '_execute_command_' + command, False) + if command_callback: + command_callback(**kwargs) + + def _send_transient_message(self, partner_to, content): + """ Notifies partner_to that a message (not stored in DB) has been + written in this channel """ + self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', partner_to.id), { + 'body': "" + content + "", + 'channel_ids': [self.id], + 'info': 'transient_message', + }) + + def _define_command_help(self): + return {'help': _("Show an helper message")} + + def _execute_command_help(self, **kwargs): + partner = self.env.user.partner_id + if self.channel_type == 'channel': + msg = _("You are in channel #%s.") % self.name + if self.public == 'private': + msg += _(" This channel is private. People must be invited to join it.") + else: + channel_partners = self.env['mail.channel.partner'].search([('partner_id', '!=', partner.id), ('channel_id', '=', self.id)]) + msg = _("You are in a private conversation with @%s.") % channel_partners[0].partner_id.name + msg += _("""

+ You can mention someone by typing @username, this will grab its attention.
+ You can mention a channel by typing #channel.
+ You can execute a command by typing /command.
+ You can insert canned responses in your message by typing :shortcut.
""") + + self._send_transient_message(partner, msg) diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index 34644aee812..f2a29fc5916 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -15,6 +15,7 @@ var _t = core._t; var _lt = core._lt; var LIMIT = 100; var preview_msg_max_size = 350; // optimal for native english speakers +var ODOOBOT_ID = "ODOOBOT"; var MessageModel = new Model('mail.message', session.context); var ChannelModel = new Model('mail.channel', session.context); @@ -35,6 +36,7 @@ var needaction_counter = 0; var starred_counter = 0; var mention_partner_suggestions = []; var canned_responses = []; +var commands = []; var discuss_menu_id; var global_unread_counter = 0; var pinned_dm_partners = []; // partner_ids we have a pinned DM with @@ -126,7 +128,7 @@ function make_message (data) { is_author: data.author_id && data.author_id[0] === session.partner_id, is_note: data.is_note, is_system_notification: data.message_type === 'notification' && data.model === 'mail.channel', - attachment_ids: data.attachment_ids, + attachment_ids: data.attachment_ids || [], subject: data.subject, email_from: data.email_from, record_name: data.record_name, @@ -184,15 +186,18 @@ function make_message (data) { if ((!msg.author_id || !msg.author_id[0]) && msg.email_from) { msg.mailto = msg.email_from; } else { - msg.displayed_author = msg.author_id && msg.author_id[1] || + msg.displayed_author = (msg.author_id === ODOOBOT_ID) && "OdooBot" || + msg.author_id && msg.author_id[1] || msg.email_from || _t('Anonymous'); } - // Don't redirect on author clicked of self-posted messages - msg.author_redirect = !msg.is_author; + // Don't redirect on author clicked of self-posted or OdooBot messages + msg.author_redirect = !msg.is_author && msg.author_id !== ODOOBOT_ID; // Compute the avatar_url - if (msg.author_id && msg.author_id[0]) { + if (msg.author_id === ODOOBOT_ID) { + msg.avatar_src = "/mail/static/src/img/odoo_o.png"; + } else if (msg.author_id && msg.author_id[0]) { msg.avatar_src = "/web/image/res.partner/" + msg.author_id[0] + "/image_small"; } else if (msg.message_type === 'email') { msg.avatar_src = "/mail/static/src/img/email_icon.png"; @@ -244,6 +249,7 @@ function make_channel (data, options) { var channel = { id: data.id, name: data.name, + server_type: data.channel_type, type: data.type || data.channel_type, all_history_loaded: false, uuid: data.uuid, @@ -488,6 +494,8 @@ function on_partner_notification (data) { on_mark_as_unread_notification(data); } else if (data.info === 'channel_seen') { on_channel_seen_notification(data); + } else if (data.info === 'transient_message') { + on_transient_message_notification(data); } else { on_chat_session_notification(data); } @@ -604,6 +612,13 @@ function on_presence_notification (data) { } } +function on_transient_message_notification (data) { + var last_message = _.last(messages); + data.id = (last_message ? last_message.id : 0) + 0.01; + data.author_id = data.author_id || ODOOBOT_ID; + add_message(data); +} + // Public interface //---------------------------------------------------------------------------------- var chat_manager = { @@ -622,11 +637,12 @@ var chat_manager = { msg.subject = data.subject; } if ('channel_id' in options) { - // post a message in a channel - return ChannelModel.call('message_post', [options.channel_id], _.extend(msg, { + // post a message in a channel or execute a command + return ChannelModel.call(data.command ? 'execute_command' : 'message_post', [options.channel_id], _.extend(msg, { message_type: 'comment', content_subtype: 'html', subtype: 'mail.mt_comment', + command: data.command, })); } if ('model' in options && 'res_id' in options) { @@ -758,6 +774,11 @@ var chat_manager = { return channel.members_deferred; }, + get_commands: function (channel) { + return _.filter(commands, function (command) { + return !command.channel_types || _.contains(command.channel_types, channel.server_type); + }); + }, get_canned_responses: function () { return canned_responses; }, @@ -992,6 +1013,9 @@ function init () { }); needaction_counter = result.needaction_inbox_counter; starred_counter = result.starred_counter; + commands = _.map(result.commands, function (command) { + return _.extend({ id: command.name }, command); + }); mention_partner_suggestions = result.mention_partner_suggestions; discuss_menu_id = result.menu_id; diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index dc8e8e71e81..22c3f94f2b5 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -478,6 +478,7 @@ var Chatter = form_common.AbstractField.extend({ var old_composer = this.composer; // create the new composer this.composer = new ChatterComposer(this, this.thread_dataset, { + commands_enabled: false, context: this.context, input_min_height: 50, input_max_height: Number.MAX_VALUE, // no max_height limit for the chatter diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js index e5e6136568b..72626469a19 100644 --- a/addons/mail/static/src/js/client_action.js +++ b/addons/mail/static/src/js/client_action.js @@ -643,9 +643,11 @@ var ChatAction = Widget.extend(ControlPanelMixin, { } }, on_composer_input_focused: function () { - var suggestions = chat_manager.get_mention_partner_suggestions(this.channel); var composer = this.channel.mass_mailing ? this.extended_composer : this.basic_composer; - composer.mention_set_prefetched_partners(suggestions); + var commands = chat_manager.get_commands(this.channel); + var partners = chat_manager.get_mention_partner_suggestions(this.channel); + composer.mention_set_enabled_commands(commands); + composer.mention_set_prefetched_partners(partners); }, on_click_button_invite: function () { diff --git a/addons/mail/static/src/js/composer.js b/addons/mail/static/src/js/composer.js index 3306a9ed0b0..0edfe170011 100644 --- a/addons/mail/static/src/js/composer.js +++ b/addons/mail/static/src/js/composer.js @@ -19,6 +19,7 @@ var NON_BREAKING_SPACE = '\u00a0'; var MENTION_PARTNER_DELIMITER = '@'; var MENTION_CHANNEL_DELIMITER = '#'; var MENTION_CANNED_RESPONSE_DELIMITER = ':'; +var MENTION_COMMAND_DELIMITER = '/'; // The MentionManager allows the Composer to register listeners. For each // listener, it detects if the user is currently typing a mention (starting by a @@ -99,6 +100,7 @@ var MentionManager = Widget.extend({ // Public API /** * Registers a new listener, described by an object containing the following keys + * @param {boolean} [beginning_only] true to enable autocomplete only at first position of input * @param {char} [delimiter] the mention delimiter * @param {function} [fetch_callback] the callback to fetch mention suggestions * @param {boolean} [generate_links] true to wrap mentions in links @@ -169,8 +171,12 @@ var MentionManager = Widget.extend({ var text_val = this.composer.$input.val(); var cursor_position = this._get_selection_positions().start; var left_string = text_val.substring(0, cursor_position); - function validate_keyword (delimiter) { - var search_str = text_val.substring(left_string.lastIndexOf(delimiter) - 1, cursor_position); + function validate_keyword (delimiter, beginning_only) { + var delimiter_position = left_string.lastIndexOf(delimiter) - 1; + if (beginning_only && delimiter_position > 0) { + return false; + } + var search_str = text_val.substring(delimiter_position, cursor_position); var pattern = "(^"+delimiter+"|(^\\s"+delimiter+"))"; var regex_start = new RegExp(pattern, "g"); search_str = search_str.replace(/^\s\s*|^[\n\r]/g, ''); @@ -184,7 +190,7 @@ var MentionManager = Widget.extend({ this.active_listener = undefined; for (var i=0; i'); + var commands = this.options.commands_enabled ? this.mention_manager.get_listener_selection('/') : []; return $.when({ content: this.mention_manager.generate_links(value), attachment_ids: _.pluck(this.get('attachment_ids'), 'id'), partner_ids: _.uniq(_.pluck(this.mention_manager.get_listener_selection('@'), 'id')), + command: commands.length > 0 ? commands[0].name : undefined, }); }, @@ -689,9 +707,18 @@ var BasicComposer = Widget.extend({ return canned_responses[i]; }); }, + mention_get_commands: function (search) { + var search_regexp = new RegExp(_.str.escapeRegExp(utils.unaccent(search)), 'i'); + return _.filter(this.mention_commands, function (command) { + return search_regexp.test(command.name); + }).slice(0, this.options.mention_fetch_limit); + }, mention_set_prefetched_partners: function (prefetched_partners) { this.mention_prefetched_partners = prefetched_partners; }, + mention_set_enabled_commands: function (commands) { + this.mention_commands = commands; + }, mention_get_listener_selections: function () { return this.mention_manager.get_listener_selections(); }, diff --git a/addons/mail/static/src/js/extended_chat_window.js b/addons/mail/static/src/js/extended_chat_window.js index 3a298b95a96..02fe2b5f907 100644 --- a/addons/mail/static/src/js/extended_chat_window.js +++ b/addons/mail/static/src/js/extended_chat_window.js @@ -30,8 +30,10 @@ return ChatWindow.extend({ }); basic_composer.once('input_focused', self, function () { var channel = chat_manager.get_channel(this.channel_id); - var suggestions = chat_manager.get_mention_partner_suggestions(channel); - basic_composer.mention_set_prefetched_partners(suggestions); + var commands = chat_manager.get_commands(channel); + var partners = chat_manager.get_mention_partner_suggestions(channel); + basic_composer.mention_set_enabled_commands(commands); + basic_composer.mention_set_prefetched_partners(partners); }); def = basic_composer.replace(self.$('.o_chat_composer')); } diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js index 21a0e1bb153..446333666cc 100644 --- a/addons/mail/static/src/js/thread.js +++ b/addons/mail/static/src/js/thread.js @@ -41,7 +41,7 @@ var Thread = Widget.extend({ "click .o_thread_message_reply": function (event) { this.selected_id = $(event.currentTarget).data('message-id'); this.$('.o_thread_message').removeClass('o_thread_selected_message'); - this.$('.o_thread_message[data-message-id=' + this.selected_id + ']') + this.$('.o_thread_message[data-message-id="' + this.selected_id + '"]') .addClass('o_thread_selected_message'); this.trigger('select_message', this.selected_id); event.stopPropagation(); @@ -194,7 +194,7 @@ var Thread = Widget.extend({ remove_message_and_render: function (message_id, messages, options) { var self = this; var done = $.Deferred(); - this.$('.o_thread_message[data-message-id=' + message_id + ']').fadeOut({ + this.$('.o_thread_message[data-message-id="' + message_id + '"]').fadeOut({ done: function () { self.render(messages, options); done.resolve();}, duration: 200, }); @@ -209,7 +209,7 @@ var Thread = Widget.extend({ scroll_to: function (options) { options = options || {}; if (options.id !== undefined) { - var $target = this.$('.o_thread_message[data-message-id=' + options.id + ']'); + var $target = this.$('.o_thread_message[data-message-id="' + options.id + '"]'); if (options.only_if_necessary) { var delta = $target.parent().height() - $target.height(); var offset = delta < 0 ? 0 : delta - ($target.offset().top - $target.offsetParent().offset().top); diff --git a/addons/mail/static/src/xml/composer.xml b/addons/mail/static/src/xml/composer.xml index 9e248755a56..ec7e0559d46 100644 --- a/addons/mail/static/src/xml/composer.xml +++ b/addons/mail/static/src/xml/composer.xml @@ -75,5 +75,11 @@ + + + / + + +