[ADD] mail: commands support
Use the mention mechanism to autocomplete on commands (keyword /). A first command /help has been implemented.
This commit is contained in:
@@ -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'),
|
||||
|
||||
@@ -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': "<span class='o_mail_notification'>" + content + "</span>",
|
||||
'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 <b>#%s</b>.") % 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 <b>@%s</b>.") % channel_partners[0].partner_id.name
|
||||
msg += _("""<br><br>
|
||||
You can mention someone by typing <b>@username</b>, this will grab its attention.<br>
|
||||
You can mention a channel by typing <b>#channel</b>.<br>
|
||||
You can execute a command by typing <b>/command</b>.<br>
|
||||
You can insert canned responses in your message by typing <b>:shortcut</b>.<br>""")
|
||||
|
||||
self._send_transient_message(partner, msg)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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 <a> 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<this.listeners.length; i++) {
|
||||
var listener = this.listeners[i];
|
||||
this.mention_word = validate_keyword(listener.delimiter);
|
||||
this.mention_word = validate_keyword(listener.delimiter, listener.beginning_only);
|
||||
|
||||
if (this.mention_word !== false) {
|
||||
this.active_listener = listener;
|
||||
@@ -355,6 +361,7 @@ var BasicComposer = Widget.extend({
|
||||
init: function (parent, options) {
|
||||
this._super.apply(this, arguments);
|
||||
this.options = _.defaults(options || {}, {
|
||||
commands_enabled: true,
|
||||
context: {},
|
||||
input_baseline: 18,
|
||||
input_max_height: 150,
|
||||
@@ -398,6 +405,15 @@ var BasicComposer = Widget.extend({
|
||||
selection: this.options.default_mention_selections[MENTION_CANNED_RESPONSE_DELIMITER],
|
||||
suggestion_template: 'mail.MentionCannedResponseSuggestions',
|
||||
});
|
||||
if (this.options.commands_enabled) {
|
||||
this.mention_manager.register({
|
||||
beginning_only: true,
|
||||
delimiter: MENTION_COMMAND_DELIMITER,
|
||||
fetch_callback: this.mention_get_commands.bind(this),
|
||||
selection: this.options.default_mention_selections[MENTION_COMMAND_DELIMITER],
|
||||
suggestion_template: 'mail.MentionCommandSuggestions',
|
||||
});
|
||||
}
|
||||
|
||||
// Emojis
|
||||
this.emoji_container_classname = 'o_composer_emoji';
|
||||
@@ -453,10 +469,12 @@ var BasicComposer = Widget.extend({
|
||||
// Return a deferred as this function is extended with asynchronous
|
||||
// behavior for the chatter composer
|
||||
var value = _.escape(this.$input.val()).replace(/\n|\r/g, '<br/>');
|
||||
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();
|
||||
},
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -75,5 +75,11 @@
|
||||
<span class="o_mention_info"><t t-esc="suggestion.substitution"/></span>
|
||||
</t>
|
||||
</t>
|
||||
<t t-name="mail.MentionCommandSuggestions" t-extend="mail.AbstractMentionSuggestions">
|
||||
<t t-jquery=".o_mention_proposition a" t-operation="append">
|
||||
<span class="o_mention_name">/<t t-esc="suggestion.name"/></span>
|
||||
<span class="o_mention_info"><t t-esc="suggestion.help"/></span>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
Reference in New Issue
Block a user