diff --git a/addons/im_livechat/__openerp__.py b/addons/im_livechat/__openerp__.py index 030230dcc95..39d4c2bf10e 100644 --- a/addons/im_livechat/__openerp__.py +++ b/addons/im_livechat/__openerp__.py @@ -30,6 +30,7 @@ Help your customers with this chat, and analyse their feedback. ], 'demo': [ "data/im_livechat_channel_demo.xml", + 'data/mail_shortcode_demo.xml', ], 'depends': ["mail", "rating"], 'qweb': [ diff --git a/addons/im_livechat/data/mail_shortcode_demo.xml b/addons/im_livechat/data/mail_shortcode_demo.xml new file mode 100644 index 00000000000..748fb65ba47 --- /dev/null +++ b/addons/im_livechat/data/mail_shortcode_demo.xml @@ -0,0 +1,18 @@ + + + + + + hello + Hello. How may I help you? + text + + + + bye + Thanks for your feedback. Good bye! + text + + + + diff --git a/addons/im_livechat/views/im_livechat_channel_views.xml b/addons/im_livechat/views/im_livechat_channel_views.xml index 9b2b17259f0..b7a823f82e0 100644 --- a/addons/im_livechat/views/im_livechat_channel_views.xml +++ b/addons/im_livechat/views/im_livechat_channel_views.xml @@ -174,6 +174,35 @@ + + + im_livechat.canned_response.tree + mail.shortcode + + + + + + + + + + Canned Responses + mail.shortcode + tree + + [('shortcode_type','=','text')] + +

+ Click to create a new canned response. +

+ Canned responses allow you to insert prewritten responses in + your messages by typing :shortcut. The shortcut is + replaced directly in your message, so that you can still edit + it before sending. +

+
+
+ + diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py index 6aa5d854aaa..3e763c72b39 100644 --- a/addons/mail/controllers/main.py +++ b/addons/mail/controllers/main.py @@ -243,7 +243,7 @@ class MailController(http.Controller): 'starred_counter': request.env['res.partner'].get_starred_count(), 'channel_slots': request.env['mail.channel'].channel_fetch_slot(), 'mention_partner_suggestions': request.env['res.partner'].get_static_mention_suggestions(), - 'emoji': request.env['mail.shortcode'].sudo().search_read([('shortcode_type', '=', 'image')], ['source', 'substitution', 'description']), + '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'), } return values diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 41fc8e0d551..02607bfcbc3 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -213,8 +213,6 @@ class Channel(models.Model): def message_post(self, body='', subject=None, message_type='notification', subtype=None, parent_id=False, attachments=None, content_subtype='html', **kwargs): # auto pin 'direct_message' channel partner self.filtered(lambda channel: channel.channel_type == 'chat').mapped('channel_last_seen_partner_ids').write({'is_pinned': True}) - # apply shortcode (text only) subsitution - body = self.env['mail.shortcode'].apply_shortcode(body, shortcode_type='text') message = super(Channel, self.with_context(mail_create_nosubscribe=True)).message_post(body=body, subject=subject, message_type=message_type, subtype=subtype, parent_id=parent_id, attachments=attachments, content_subtype=content_subtype, **kwargs) return message diff --git a/addons/mail/models/mail_shortcode.py b/addons/mail/models/mail_shortcode.py index 478fb7be2ef..8c659a1f134 100644 --- a/addons/mail/models/mail_shortcode.py +++ b/addons/mail/models/mail_shortcode.py @@ -15,12 +15,12 @@ class MailShortcode(models.Model): _name = 'mail.shortcode' _description = 'Canned Response / Shortcode' - source = fields.Char('Shortcut', required=True, index=True, help="The shortcut which must be replace in the Chat Messages") - substitution = fields.Char('Substitution', required=True, index=True, help="The escaped html code replacing the shortcut") + source = fields.Char('Shortcut', required=True, index=True, help="The shortcut which must be replaced in the Chat Messages") + substitution = fields.Text('Substitution', required=True, index=True, help="The escaped html code replacing the shortcut") description = fields.Char('Description') shortcode_type = fields.Selection([('image', 'Smiley'), ('text', 'Canned Response')], required=True, default='text', help="* Smiley are only used for HTML code to display an image "\ - "* Text (default value) is use to substitute text with another text") + "* Text (default value) is used to substitute text with another text") @api.model def create(self, values): @@ -43,17 +43,3 @@ class MailShortcode(models.Model): if is_img_tag: return substitution return cgi.escape(substitution) - - @api.model - def apply_shortcode(self, message, shortcode_type=None): - """ Apply the substitution on the given text - :param message : the text where the shortcode will be applied - :param shortcode_type : shortcode type that should be applied during the substitution - """ - domain = [] - if shortcode_type: - domain.append(('shortcode_type', '=', shortcode_type)) - for shortcode in self.search(domain): - regex = '(?:^|\s)(%s)(?:\s|$)' % re.escape(shortcode.source) - message = re.sub(regex, " " + shortcode.substitution + " ", message) - return message diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index 69088701bc1..34644aee812 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -34,6 +34,7 @@ var emoji_substitutions = {}; var needaction_counter = 0; var starred_counter = 0; var mention_partner_suggestions = []; +var canned_responses = []; var discuss_menu_id; var global_unread_counter = 0; var pinned_dm_partners = []; // partner_ids we have a pinned DM with @@ -757,6 +758,10 @@ var chat_manager = { return channel.members_deferred; }, + get_canned_responses: function () { + return canned_responses; + }, + get_emojis: function() { return emojis; }, @@ -988,12 +993,18 @@ function init () { needaction_counter = result.needaction_inbox_counter; starred_counter = result.starred_counter; mention_partner_suggestions = result.mention_partner_suggestions; - emojis = result.emoji; - _.each(emojis, function(emoji) { - emoji_substitutions[_.escape(emoji.source)] = emoji.substitution; - }); discuss_menu_id = result.menu_id; + // Shortcodes: canned responses and emojis + _.each(result.shortcodes, function (s) { + if (s.shortcode_type === 'text') { + canned_responses.push(_.pick(s, ['id', 'source', 'substitution'])); + } else { + emojis.push(_.pick(s, ['id', 'source', 'substitution', 'description'])); + emoji_substitutions[_.escape(s.source)] = s.substitution; + } + }); + bus.start_polling(); }); } diff --git a/addons/mail/static/src/js/composer.js b/addons/mail/static/src/js/composer.js index af9efe25859..3306a9ed0b0 100644 --- a/addons/mail/static/src/js/composer.js +++ b/addons/mail/static/src/js/composer.js @@ -18,6 +18,7 @@ var NON_BREAKING_SPACE = '\u00a0'; var MENTION_PARTNER_DELIMITER = '@'; var MENTION_CHANNEL_DELIMITER = '#'; +var MENTION_CANNED_RESPONSE_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 @@ -59,8 +60,12 @@ var MentionManager = Widget.extend({ var selected_suggestion = _.find(_.flatten(this.get('mention_suggestions')), function (s) { return s.id === id; }); - // replace white spaces with non-breaking spaces to facilitate mentions detection in text - selected_suggestion.name = selected_suggestion.name.replace(/ /g, NON_BREAKING_SPACE); + var substitution = selected_suggestion.substitution; + if (!substitution) { // no substitution string given, so use the mention name instead + // replace white spaces with non-breaking spaces to facilitate mentions detection in text + selected_suggestion.name = selected_suggestion.name.replace(/ /g, NON_BREAKING_SPACE); + substitution = this.active_listener.delimiter + selected_suggestion.name; + } var get_mention_index = function (matches, cursor_position) { for (var i=0; i wrapping the mention + * @param {boolean} [generate_links] true to wrap mentions in links + * @param {string} [model] (optional) the model used for redirection + * @param {string} [redirect_classname] (optional) the classname of the wrapping the mention + * @param {array} [selection] (optional) initial mentions for each listener + * @param {string} [suggestion_template] the template of suggestions' dropdown */ register: function (listener) { this.listeners.push(_.defaults(listener, { + model: '', + redirect_classname: '', selection: [], })); }, @@ -202,6 +213,9 @@ var MentionManager = Widget.extend({ var base_href = session.url("/web"); var mention_link = "%s%s"; _.each(this.listeners, function (listener) { + if (!listener.generate_links) { + return; + } var selection = listener.selection; if (selection.length) { var matches = self._get_match(s, listener); @@ -266,12 +280,29 @@ var MentionManager = Widget.extend({ return result; }, _render_suggestions: function () { - if (_.flatten(this.get('mention_suggestions')).length) { - this.$el.html(QWeb.render('mail.ChatComposer.MentionSuggestions', { - suggestions: this.get('mention_suggestions'), + var suggestions = []; + if (_.find(this.get('mention_suggestions'), _.isArray)) { + // Array of arrays -> Flatten and insert dividers between groups + var insert_divider = false; + _.each(this.get('mention_suggestions'), function (suggestion_group) { + if (suggestion_group.length > 0) { + if (insert_divider) { + suggestions.push({ divider: true }); + } + suggestions = suggestions.concat(suggestion_group); + insert_divider = true; + } + }); + } else { + suggestions = this.get('mention_suggestions'); + } + if (suggestions.length) { + this.$el.html(QWeb.render(this.active_listener.suggestion_template, { + suggestions: suggestions, })); this.$el .addClass('open') + .find('ul').css("max-width", this.composer.$input.width()) .find('.o_mention_proposition').first().addClass('active'); this.open = true; } else { @@ -346,16 +377,26 @@ var BasicComposer = Widget.extend({ this.mention_manager.register({ delimiter: MENTION_PARTNER_DELIMITER, fetch_callback: this.mention_fetch_partners.bind(this), + generate_links: true, model: 'res.partner', redirect_classname: 'o_mail_redirect', selection: this.options.default_mention_selections[MENTION_PARTNER_DELIMITER], + suggestion_template: 'mail.MentionPartnerSuggestions', }); this.mention_manager.register({ delimiter: MENTION_CHANNEL_DELIMITER, fetch_callback: this.mention_fetch_channels.bind(this), + generate_links: true, model: 'mail.channel', redirect_classname: 'o_channel_redirect', selection: this.options.default_mention_selections[MENTION_CHANNEL_DELIMITER], + suggestion_template: 'mail.MentionChannelSuggestions', + }); + this.mention_manager.register({ + delimiter: MENTION_CANNED_RESPONSE_DELIMITER, + fetch_callback: this.mention_get_canned_responses.bind(this), + selection: this.options.default_mention_selections[MENTION_CANNED_RESPONSE_DELIMITER], + suggestion_template: 'mail.MentionCannedResponseSuggestions', }); // Emojis @@ -621,8 +662,8 @@ var BasicComposer = Widget.extend({ _.each(prefetched_partners, function (partners) { if (limit > 0) { var filtered_partners = _.filter(partners, function (partner) { - return partner.email && partner.email.search(search_regexp) !== -1 || - partner.name && utils.unaccent(partner.name).search(search_regexp) !== -1; + return partner.email && search_regexp.test(partner.email) || + partner.name && search_regexp.test(utils.unaccent(partner.name)); }); if (filtered_partners.length) { suggestions.push(filtered_partners.slice(0, limit)); @@ -640,6 +681,14 @@ var BasicComposer = Widget.extend({ return suggestions; }); }, + mention_get_canned_responses: function (search) { + var canned_responses = chat_manager.get_canned_responses(); + var matches = fuzzy.filter(utils.unaccent(search), _.pluck(canned_responses, 'source')); + var indexes = _.pluck(matches.slice(0, this.options.mention_fetch_limit), 'index'); + return _.map(indexes, function (i) { + return canned_responses[i]; + }); + }, mention_set_prefetched_partners: function (prefetched_partners) { this.mention_prefetched_partners = prefetched_partners; }, diff --git a/addons/mail/static/src/less/composer.less b/addons/mail/static/src/less/composer.less index 31ae4725ef0..eb5be1225ac 100644 --- a/addons/mail/static/src/less/composer.less +++ b/addons/mail/static/src/less/composer.less @@ -14,6 +14,18 @@ width: 100%; resize: none; } + .o_composer_mention_dropdown .o_mention_proposition > a { + .o-text-overflow(); + width: 100%; + padding: 3px 15px; + .o_mention_name { + font-weight: 500; + } + .o_mention_info { + font-style: italic; + color: gray; + } + } } .o_composer_attachments_list { diff --git a/addons/mail/static/src/xml/composer.xml b/addons/mail/static/src/xml/composer.xml index a75708401ce..9e248755a56 100644 --- a/addons/mail/static/src/xml/composer.xml +++ b/addons/mail/static/src/xml/composer.xml @@ -46,21 +46,34 @@ - + + + + + + () + + + + + + + + + + + + + +