diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
index 22f5249c973..e569bd8e56f 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -7,6 +7,7 @@ import uuid
from openerp import _, api, fields, models, modules, tools
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
from openerp.exceptions import UserError
+from openerp.osv import expression
from openerp.addons.bus.models.bus_presence import AWAY_TIMER
@@ -147,8 +148,8 @@ class Channel(models.Model):
def action_unfollow(self):
result = self.write({'channel_partner_ids': [(3, self.env.user.partner_id.id)]})
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), self.channel_info()[0])
- notification = _('
') % (self.uuid, self.name,)
- self.message_post(body=notification, message_type="comment", subtype="mail.mt_comment")
+ notification = _('') % (self.id, self.name,)
+ self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment")
return result
@@ -485,7 +486,7 @@ class Channel(models.Model):
@api.multi
def channel_join_and_get_info(self):
self.ensure_one()
- notification = _('') % (self.uuid, self.name,)
+ notification = _('') % (self.id, self.name,)
self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment")
self.action_follow()
@@ -509,7 +510,22 @@ class Channel(models.Model):
'channel_partner_ids': [(4, self.env.user.partner_id.id)]
})
channel_info = new_channel.channel_info()[0]
- notification = _('') % (new_channel.uuid, new_channel.name,)
+ notification = _('') % (new_channel.id, new_channel.name,)
new_channel.message_post(body=notification, message_type="notification", subtype="mail.mt_comment")
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info)
return channel_info
+
+ @api.model
+ def get_mention_suggestions(self, search, limit=8):
+ """ Return 'limit'-first channels' id, name and public fields such that the name matches a
+ 'search' string. Exclude channels of type chat (DM), and private channels the current
+ user isn't registered to. """
+ domain = expression.AND([
+ [('name', 'ilike', search)],
+ [('channel_type', '!=', 'chat')],
+ expression.OR([
+ [('public', '!=', 'private')],
+ [('channel_partner_ids', 'in', [self.env.user.partner_id.id])]
+ ])
+ ])
+ return self.search_read(domain, ['id', 'name', 'public'], limit=limit)
diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js
index 9764bdd1018..42f2056e011 100644
--- a/addons/mail/static/src/js/chat_manager.js
+++ b/addons/mail/static/src/js/chat_manager.js
@@ -22,6 +22,7 @@ var channels = [];
var emojis = [];
var emoji_substitutions = {};
var needaction_counter = 0;
+var discuss_ids = {};
// Message and channel manipulation helpers
//----------------------------------------------------------------------------------
@@ -376,6 +377,10 @@ var chat_manager = {
return needaction_counter;
},
+ get_discuss_ids: function () {
+ return discuss_ids;
+ },
+
detach_channel: function (channel) {
return ChannelModel.call("channel_minimize", [channel.uuid, true]);
},
@@ -454,6 +459,10 @@ function init () {
});
});
+ var ir_model = new Model("ir.model.data");
+ var load_menu_id = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_menu_root_chat"]);
+ var load_action_id = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_action_client_chat"]);
+
bus.on('notification', null, function (notification) {
var model = notification[0][1];
if (model === 'ir.needaction') {
@@ -498,7 +507,11 @@ function init () {
}
});
- return $.when(load_channels, load_emojis).then(function () {
+ return $.when(load_menu_id, load_action_id, load_channels, load_emojis).then(function (menu_id, action_id) {
+ discuss_ids = {
+ menu_id: menu_id,
+ action_id: action_id,
+ };
bus.start_polling();
return chat_manager;
});
diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js
index 2aaf856cd46..0d261ea9053 100644
--- a/addons/mail/static/src/js/chatter.js
+++ b/addons/mail/static/src/js/chatter.js
@@ -105,7 +105,7 @@ var Followers = form_common.AbstractField.extend({
// event: click on 'edit_subtype(pencil)' button to edit subscription
this.$el.on('click', '.o_edit_subtype', self.on_edit_subtype);
this.$el.on('click', '.o_remove_follower', self.on_remove_follower);
- this.$el.on('click', 'a[data-partner]', self.on_follower_clicked);
+ this.$el.on('click', '.o_mail_redirect', self.on_click_redirect);
},
on_edit_subtype: function (event) {
@@ -167,24 +167,11 @@ var Followers = form_common.AbstractField.extend({
}
},
- on_follower_clicked: function (event) {
+ on_click_redirect: function (event) {
event.preventDefault();
- var partner_id = $(event.target).data('partner');
- var state = {
- 'model': 'res.partner',
- 'id': partner_id,
- 'title': this.record_name
- };
- web_client.action_manager.do_push_state(state);
- var action = {
- type:'ir.actions.act_window',
- view_type: 'form',
- view_mode: 'form',
- res_model: 'res.partner',
- views: [[false, 'form']],
- res_id: partner_id,
- };
- this.do_action(action);
+ var res_id = $(event.target).data('oe-id');
+ var res_model = $(event.target).data('oe-model');
+ this.trigger('redirect', res_model, res_id);
},
read_value: function () {
@@ -697,7 +684,6 @@ var Chatter = form_common.AbstractField.extend({
template: 'mail.Chatter',
events: {
- "click .o_mail_redirect": "on_click_redirect",
"click .o_chatter_button_new_message": "on_open_composer_new_message",
"click .o_chatter_button_log_note": "on_open_composer_log_note",
},
@@ -717,6 +703,7 @@ var Chatter = form_common.AbstractField.extend({
this.followers = this.field_manager.fields.message_follower_ids;
if (this.followers) {
this.$('.o_chatter_topbar').append(this.followers.$el);
+ this.followers.on('redirect', this, this.on_redirect);
}
this.thread = new ChatThread(this, {
@@ -728,6 +715,8 @@ var Chatter = form_common.AbstractField.extend({
this.thread.on('toggle_star_status', this, function (message_id) {
chat_manager.toggle_star_status(message_id);
});
+ this.thread.on('redirect', this, this.on_redirect);
+ this.thread.on('redirect_to_channel', this, this.on_channel_redirect);
var def1 = this._super.apply(this, arguments);
var def2 = this.thread.appendTo(this.$el);
@@ -778,15 +767,31 @@ var Chatter = form_common.AbstractField.extend({
}
},
- on_click_redirect: function (event) {
+ on_channel_redirect: function (channel_id) {
+ event.preventDefault();
+ var self = this;
+ var def;
+ var channel = chat_manager.get_channel(channel_id);
+ // If not registered to 'channel' yet, do it
+ if (channel.id !== channel_id) {
+ def = chat_manager.join_channel(channel_id);
+ }
+ $.when(def).then(function () {
+ // Execute Discuss client action with 'channel' as default channel
+ var discuss_ids = chat_manager.get_discuss_ids();
+ self.do_action(discuss_ids.action_id, {active_id: channel_id});
+ });
+ },
+
+ on_redirect: function (res_model, res_id) {
event.preventDefault();
this.do_action({
type:'ir.actions.act_window',
view_type: 'form',
view_mode: 'form',
- res_model: $(event.target).data('oe-model'),
+ res_model: res_model,
views: [[false, 'form']],
- res_id: $(event.target).data('oe-id'),
+ res_id: res_id,
});
},
diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js
index 4f161f3d93c..5113ebff5d6 100644
--- a/addons/mail/static/src/js/client_action.js
+++ b/addons/mail/static/src/js/client_action.js
@@ -118,11 +118,12 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop();
},
- init: function(parent, action) {
+ init: function(parent, action, options) {
this._super.apply(this, arguments);
this.action_manager = parent;
this.domain = [];
this.action = action;
+ this.options = options || {};
this.channels_scrolltop = {};
},
@@ -137,7 +138,8 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
};
var dataset = new data.DataSetSearch(this, 'mail.message');
var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false;
- var default_channel_id = this.action.context.active_id ||
+ var default_channel_id = this.options.active_id ||
+ this.action.context.active_id ||
this.action.params.default_active_id ||
'channel_inbox';
this.searchview = new SearchView(this, dataset, view_id, {}, options);
@@ -164,7 +166,12 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
this.thread.on('redirect', this, this.on_redirect);
this.thread.on('redirect_to_channel', this, function (channel_id) {
- this.set_channel(chat_manager.get_channel(channel_id));
+ var channel = chat_manager.get_channel(channel_id);
+ if (channel.id === channel_id) {
+ this.set_channel(channel);
+ } else {
+ chat_manager.join_channel(channel_id);
+ }
});
this.thread.on('load_more_messages', this, this.load_more_messages);
this.thread.on('mark_as_read', this, function (message_id) {
diff --git a/addons/mail/static/src/js/composer.js b/addons/mail/static/src/js/composer.js
index 7ed8318594a..b53221dac35 100644
--- a/addons/mail/static/src/js/composer.js
+++ b/addons/mail/static/src/js/composer.js
@@ -12,6 +12,296 @@ var Widget = require('web.Widget');
var QWeb = core.qweb;
var _t = core._t;
+// The MentionManager allows the Composer to register listeners. For each
+// listener, it detects if the user is currently typing a mention (starting by a
+// given delimiter). If so, if fetches mention suggestions and renders them. On
+// suggestion clicked, it updates the selection for the corresponding listener.
+var MentionManager = Widget.extend({
+ className: 'dropup o_composer_mention_dropdown',
+
+ events: {
+ "mouseover .o_mention_proposition": "on_hover_mention_proposition",
+ "click .o_mention_proposition": "on_click_mention_item",
+ },
+
+ init: function (parent, options) {
+ this._super.apply(this, arguments);
+
+ this.composer = parent;
+ this.options = _.extend({}, options, {
+ min_length: 0,
+ typing_speed: 200,
+ });
+
+ this.open = false;
+ this.listeners = [];
+ this.set('mention_suggestions', []);
+ this.on('change:mention_suggestions', this, this._render_suggestions);
+ },
+
+ // Events
+ on_hover_mention_proposition: function (event) {
+ var $elem = $(event.currentTarget);
+ this.$('.o_mention_proposition').removeClass('active');
+ $elem.addClass('active');
+ },
+ on_click_mention_item: function (event) {
+ event.preventDefault();
+
+ var text_input = this.composer.$input.val();
+ var id = $(event.currentTarget).data('id');
+ var selected_suggestion = _.find(_.flatten(this.get('mention_suggestions')), function (s) {
+ return s.id === id;
+ });
+ var get_mention_index = function (matches, cursor_position) {
+ for (var i=0; i wrapping the mention
+ * @param {boolean} [sort_selection] true if the selection must be sorted
+ * @param {string} [suggestions_template] the QWeb template to render suggestions
+ */
+ register: function (listener) {
+ this.listeners.push(_.extend(listener, {
+ selection: [],
+ }));
+ },
+
+ /**
+ * Returns true if the mention suggestions dropdown is open, false otherwise
+ */
+ is_open: function () {
+ return this.open;
+ },
+
+ proposition_navigation: function (keycode) {
+ var $active = this.$('.o_mention_proposition.active');
+ if (keycode === $.ui.keyCode.ENTER) { // selecting proposition
+ $active.click();
+ } else { // navigation in propositions
+ var $to;
+ if (keycode === $.ui.keyCode.DOWN) {
+ $to = $active.nextAll('.o_mention_proposition').first();
+ } else {
+ $to = $active.prevAll('.o_mention_proposition').first();
+ }
+ if ($to.length) {
+ $active.removeClass('active');
+ $to.addClass('active');
+ }
+ }
+ },
+
+ /**
+ * Detects if the user is currently typing a mention word
+ * @return the search string if it is, false otherwise
+ */
+ detect_delimiter: function () {
+ var self = this;
+ var options = this.options;
+ 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);
+ var pattern = "(^"+delimiter+"|(^\\s"+delimiter+"))";
+ var regex_start = new RegExp(pattern, "g");
+ search_str = search_str.replace(/^\s\s*|^[\n\r]/g, '');
+ if (regex_start.test(search_str) && search_str.length > options.min_length) {
+ search_str = search_str.replace(pattern, '');
+ return search_str.indexOf(' ') < 0 && !/[\r\n]/.test(search_str) ? search_str.replace(delimiter, '') : false;
+ }
+ return false;
+ }
+
+ this.active_listener = undefined;
+ for (var i=0; i');
return $.when({
- content: this.mention_preprocess_message(value),
+ content: this.mention_manager.generate_links(value),
attachment_ids: _.pluck(this.get('attachment_ids'), 'id'),
partner_ids: _.pluck(this.get('mention_selected_partners'), 'id'),
});
@@ -114,7 +414,7 @@ var Composer = Widget.extend({
// Empty input, selected partners and attachments
self.$input.val('');
self.resize_input();
- self.set('mention_selected_partners', []);
+ self.mention_manager.reset_selections();
self.set('attachment_ids', []);
self.$input.focus();
@@ -157,18 +457,18 @@ var Composer = Widget.extend({
// UP, DOWN: prevent moving cursor if navigation in mention propositions
case $.ui.keyCode.UP:
case $.ui.keyCode.DOWN:
- if (this.mention_dropdown_open) {
+ if (this.mention_manager.is_open()) {
event.preventDefault();
}
break;
// BACKSPACE, DELETE: check if need to remove a mention
case $.ui.keyCode.BACKSPACE:
case $.ui.keyCode.DELETE:
- this.mention_check_remove();
+ this.mention_manager.check_remove();
break;
// ENTER: submit the message only if the dropdown mention proposition is not displayed
case $.ui.keyCode.ENTER:
- if (this.mention_dropdown_open) {
+ if (this.mention_manager.is_open()) {
event.preventDefault();
} else if (!this.prevent_send(event)) {
event.preventDefault();
@@ -195,16 +495,11 @@ var Composer = Widget.extend({
case $.ui.keyCode.ENTER:
case $.ui.keyCode.UP:
case $.ui.keyCode.DOWN:
- this.mention_proposition_navigation(event.which);
+ this.mention_manager.proposition_navigation(event.which);
break;
// Otherwise, check if a mention is typed
default:
- this.mention_word = this.mention_detect_delimiter();
- if (this.mention_word !== false) {
- this.mention_word_changed();
- } else {
- this.set('mention_partners', []); // close the dropdown
- }
+ this.mention_manager.detect_delimiter();
this.resize_input();
}
},
@@ -294,226 +589,35 @@ var Composer = Widget.extend({
},
// Mention
- on_click_mention_item: function (event) {
- event.preventDefault();
-
- var text_input = this.$input.val();
- var partner_id = $(event.currentTarget).data('partner-id');
- var selected_partner = _.filter(_.flatten(this.get('mention_partners')), function (p) {
- return p.id === partner_id;
- })[0];
-
- // add the mention partner to the list
- var mention_selected_partners = this.get('mention_selected_partners');
- if (mention_selected_partners.length) { // there are already mention partners
- // get mention matches (ordered by index in the text)
- var matches = this.mention_get_match(text_input);
- var index = this.mention_get_index(matches, this.get_selection_positions().start);
- mention_selected_partners.splice(index, 0, selected_partner);
- mention_selected_partners = _.clone(mention_selected_partners);
- } else { // this is the first mentionned partner
- mention_selected_partners = mention_selected_partners.concat([selected_partner]);
- }
- this.set('mention_selected_partners', mention_selected_partners);
-
- // update input text, and reset dropdown
- var cursor_position = this.get_selection_positions().start;
- var text_left = text_input.substring(0, cursor_position-(this.mention_word.length+1));
- var text_right = text_input.substring(cursor_position, text_input.length);
- var text_input_new = text_left + this.options.mention_delimiter + selected_partner.name + ' ' + text_right;
- this.$input.val(text_input_new);
- this.set_cursor_position(text_left.length+selected_partner.name.length+2);
- this.set('mention_partners', []);
- },
-
- on_hover_mention_proposition: function (event) {
- var $elem = $(event.currentTarget);
- this.$('.o_mention_proposition').removeClass('active');
- $elem.addClass('active');
- },
-
- mention_proposition_navigation: function (keycode) {
- var $active = this.$('.o_mention_proposition.active');
- if (keycode === $.ui.keyCode.ENTER) { // selecting proposition
- $active.click();
- } else { // navigation in propositions
- var $to;
- if (keycode === $.ui.keyCode.DOWN) {
- $to = $active.nextAll('.o_mention_proposition').first();
- } else {
- $to = $active.prevAll('.o_mention_proposition').first();
- }
- if ($to.length) {
- $active.removeClass('active');
- $to.addClass('active');
- }
- }
- },
-
- /**
- * Return the text attached to the mention delimiter
- * @returns {String|false}: the text right after the delimiter or false
- */
- mention_detect_delimiter: function () {
- var options = this.options;
- var delimiter = options.mention_delimiter;
- var text_val = this.$input.val();
- var cursor_position = this.get_selection_positions().start;
- var left_string = text_val.substring(0, cursor_position);
- var search_str = text_val.substring(left_string.lastIndexOf(delimiter) - 1, cursor_position);
-
- return validate_keyword(search_str);
-
- function validate_keyword (search_str) {
- var pattern = "(^"+delimiter+"|(^\\s"+delimiter+"))";
- var regex_start = new RegExp(pattern, "g");
- search_str = search_str.replace(/^\s\s*|^[\n\r]/g, '');
- if (regex_start.test(search_str) && search_str.length > options.mention_min_length) {
- search_str = search_str.replace(pattern, '');
- return search_str.indexOf(' ') < 0 && !/[\r\n]/.test(search_str) ? search_str.replace(delimiter, '') : false;
- }
- return false;
- }
- },
-
- mention_word_changed: function () {
- var self = this;
- // start a timeout to fetch partner with the current 'mention word'. The timer avoid to start
- // an RPC for each pushed key when the user is typing the partner name.
- // The 'mention_typing_speed' option should approach the time for a human to type a letter.
- clearTimeout(this.mention_fetch_timer);
- this.mention_fetch_timer = setTimeout(function () {
- self.mention_fetch_partner(self.mention_word);
- }, this.options.mention_typing_speed);
- },
-
- mention_fetch_partner: function (search) {
- var self = this;
+ mention_fetch_channels: function (search) {
var kwargs = {
- channel: this.get_channel_info(),
- exclude: _.pluck(this.get('mention_selected_partners'), 'id'),
limit: this.options.mention_fetch_limit,
search: search,
};
- this.PartnerModel.call('get_mention_suggestions', kwargs).then(function (suggestions) {
- self.set('mention_partners', suggestions);
- });
+ return this.ChannelModel
+ .call('get_mention_suggestions', kwargs)
+ .then(function (suggestions) {
+ return _.partition(suggestions, function (suggestion) {
+ return _.contains(['public', 'groups'], suggestion.public);
+ });
+ });
},
-
- mention_check_remove: function () {
- var mention_selected_partners = this.get('mention_selected_partners');
- var partners_to_remove = [];
- var selection = this.get_selection_positions();
- var deleted_binf = selection.start;
- var deleted_bsup = selection.end;
-
- var matches = this.mention_get_match(this.$input.val());
- for (var i=0; i@%s", partners[i].id, partner_name);
- var subtext = message.substring(start_index, end_index).replace(match[0], processed_text);
- substrings.push(subtext);
- start_index = end_index;
- }
- substrings.push(message.substring(start_index, message.length));
- return substrings.join('');
- }
- return message;
- },
-
- render_mention_partners: function () {
- if (_.flatten(this.get('mention_partners')).length) {
- this.$mention_dropdown.html(QWeb.render('mail.ChatComposer.MentionMenu', {
- suggestions: this.get('mention_partners'),
- }));
- this.$mention_dropdown
- .addClass('open')
- .find('.o_mention_proposition').first().addClass('active');
- this.mention_dropdown_open = true;
- } else {
- this.$mention_dropdown.removeClass('open');
- this.$mention_dropdown.empty();
- this.mention_dropdown_open = false;
- }
- },
-
- render_mention_selected_partners: function () {
+ mention_render_selection: function (listener) {
this.$mention_partner_tags.html(QWeb.render('mail.ChatComposer.MentionTags', {
- partners: this.get('mention_selected_partners'),
+ partners: listener.selection,
}));
},
- /**
- * Return the matches (as RexExp.exec does) for the partner mention in the input text
- * @param {String} input_text: the text to search matches
- * @returns {Object[]} matches in the same format as RexExp.exec()
- */
- mention_get_match: function (input_text) {
- var self = this;
- // create the regex of all mention partner name
- var partner_names = _.pluck(this.get('mention_selected_partners'), 'name');
- var escaped_partner_names = _.map(partner_names, function (str) {
- return "("+_.str.escapeRegExp(self.options.mention_delimiter+str)+")";
- });
- var regex_str = escaped_partner_names.join('|');
- // extract matches
- var result = [];
- if(regex_str.length){
- var myRegexp = new RegExp(regex_str, 'g');
- var match = myRegexp.exec(input_text);
- while (match !== null) {
- result.push(match);
- match = myRegexp.exec(input_text);
- }
- }
- return result;
- },
-
- mention_get_index: function (matches, cursor_position) {
- for (var i=0; i