[IMP] mail: composer: mention channels feature
Generalizes the mention mechanism to allow mentionning partners (@) and channels (#). When a user clicks on a mentionned channel, it subscribes to it and joins it. If the mention is in a chatter, the user is redirected to the Discuss client action. Moves the RPCs to get the menu and action id of the Discuss client action from systray.js to chat_managaer.js as they are now needed in the chatter as well. Also fixes the on_channel_clicked redirection for notification message (created/joined/left #channel). Those three messages are now of type notification, and correctly redirect on click.
This commit is contained in:
@@ -7,6 +7,7 @@ import uuid
|
|||||||
from openerp import _, api, fields, models, modules, tools
|
from openerp import _, api, fields, models, modules, tools
|
||||||
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
|
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
|
||||||
from openerp.exceptions import UserError
|
from openerp.exceptions import UserError
|
||||||
|
from openerp.osv import expression
|
||||||
|
|
||||||
from openerp.addons.bus.models.bus_presence import AWAY_TIMER
|
from openerp.addons.bus.models.bus_presence import AWAY_TIMER
|
||||||
|
|
||||||
@@ -147,8 +148,8 @@ class Channel(models.Model):
|
|||||||
def action_unfollow(self):
|
def action_unfollow(self):
|
||||||
result = self.write({'channel_partner_ids': [(3, self.env.user.partner_id.id)]})
|
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])
|
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), self.channel_info()[0])
|
||||||
notification = _('<div class="o_mail_notification">left <a href="#" data-channel-id="%s">#%s</a></div>') % (self.uuid, self.name,)
|
notification = _('<div class="o_mail_notification">left <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (self.id, self.name,)
|
||||||
self.message_post(body=notification, message_type="comment", subtype="mail.mt_comment")
|
self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -485,7 +486,7 @@ class Channel(models.Model):
|
|||||||
@api.multi
|
@api.multi
|
||||||
def channel_join_and_get_info(self):
|
def channel_join_and_get_info(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
notification = _('<div class="o_mail_notification">joined <a href="#" data-channel-id="%s">#%s</a></div>') % (self.uuid, self.name,)
|
notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (self.id, self.name,)
|
||||||
self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment")
|
self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment")
|
||||||
self.action_follow()
|
self.action_follow()
|
||||||
|
|
||||||
@@ -509,7 +510,22 @@ class Channel(models.Model):
|
|||||||
'channel_partner_ids': [(4, self.env.user.partner_id.id)]
|
'channel_partner_ids': [(4, self.env.user.partner_id.id)]
|
||||||
})
|
})
|
||||||
channel_info = new_channel.channel_info()[0]
|
channel_info = new_channel.channel_info()[0]
|
||||||
notification = _('<div class="o_mail_notification">created <a href="#" data-channel-id="%s">#%s</a></div>') % (new_channel.uuid, new_channel.name,)
|
notification = _('<div class="o_mail_notification">created <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (new_channel.id, new_channel.name,)
|
||||||
new_channel.message_post(body=notification, message_type="notification", subtype="mail.mt_comment")
|
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)
|
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info)
|
||||||
return 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)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ var channels = [];
|
|||||||
var emojis = [];
|
var emojis = [];
|
||||||
var emoji_substitutions = {};
|
var emoji_substitutions = {};
|
||||||
var needaction_counter = 0;
|
var needaction_counter = 0;
|
||||||
|
var discuss_ids = {};
|
||||||
|
|
||||||
// Message and channel manipulation helpers
|
// Message and channel manipulation helpers
|
||||||
//----------------------------------------------------------------------------------
|
//----------------------------------------------------------------------------------
|
||||||
@@ -376,6 +377,10 @@ var chat_manager = {
|
|||||||
return needaction_counter;
|
return needaction_counter;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get_discuss_ids: function () {
|
||||||
|
return discuss_ids;
|
||||||
|
},
|
||||||
|
|
||||||
detach_channel: function (channel) {
|
detach_channel: function (channel) {
|
||||||
return ChannelModel.call("channel_minimize", [channel.uuid, true]);
|
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) {
|
bus.on('notification', null, function (notification) {
|
||||||
var model = notification[0][1];
|
var model = notification[0][1];
|
||||||
if (model === 'ir.needaction') {
|
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();
|
bus.start_polling();
|
||||||
return chat_manager;
|
return chat_manager;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ var Followers = form_common.AbstractField.extend({
|
|||||||
// event: click on 'edit_subtype(pencil)' button to edit subscription
|
// 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_edit_subtype', self.on_edit_subtype);
|
||||||
this.$el.on('click', '.o_remove_follower', self.on_remove_follower);
|
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) {
|
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();
|
event.preventDefault();
|
||||||
var partner_id = $(event.target).data('partner');
|
var res_id = $(event.target).data('oe-id');
|
||||||
var state = {
|
var res_model = $(event.target).data('oe-model');
|
||||||
'model': 'res.partner',
|
this.trigger('redirect', res_model, res_id);
|
||||||
'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);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
read_value: function () {
|
read_value: function () {
|
||||||
@@ -697,7 +684,6 @@ var Chatter = form_common.AbstractField.extend({
|
|||||||
template: 'mail.Chatter',
|
template: 'mail.Chatter',
|
||||||
|
|
||||||
events: {
|
events: {
|
||||||
"click .o_mail_redirect": "on_click_redirect",
|
|
||||||
"click .o_chatter_button_new_message": "on_open_composer_new_message",
|
"click .o_chatter_button_new_message": "on_open_composer_new_message",
|
||||||
"click .o_chatter_button_log_note": "on_open_composer_log_note",
|
"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;
|
this.followers = this.field_manager.fields.message_follower_ids;
|
||||||
if (this.followers) {
|
if (this.followers) {
|
||||||
this.$('.o_chatter_topbar').append(this.followers.$el);
|
this.$('.o_chatter_topbar').append(this.followers.$el);
|
||||||
|
this.followers.on('redirect', this, this.on_redirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.thread = new ChatThread(this, {
|
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) {
|
this.thread.on('toggle_star_status', this, function (message_id) {
|
||||||
chat_manager.toggle_star_status(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 def1 = this._super.apply(this, arguments);
|
||||||
var def2 = this.thread.appendTo(this.$el);
|
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();
|
event.preventDefault();
|
||||||
this.do_action({
|
this.do_action({
|
||||||
type:'ir.actions.act_window',
|
type:'ir.actions.act_window',
|
||||||
view_type: 'form',
|
view_type: 'form',
|
||||||
view_mode: 'form',
|
view_mode: 'form',
|
||||||
res_model: $(event.target).data('oe-model'),
|
res_model: res_model,
|
||||||
views: [[false, 'form']],
|
views: [[false, 'form']],
|
||||||
res_id: $(event.target).data('oe-id'),
|
res_id: res_id,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -118,11 +118,12 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
|||||||
this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop();
|
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._super.apply(this, arguments);
|
||||||
this.action_manager = parent;
|
this.action_manager = parent;
|
||||||
this.domain = [];
|
this.domain = [];
|
||||||
this.action = action;
|
this.action = action;
|
||||||
|
this.options = options || {};
|
||||||
this.channels_scrolltop = {};
|
this.channels_scrolltop = {};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -137,7 +138,8 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
|||||||
};
|
};
|
||||||
var dataset = new data.DataSetSearch(this, 'mail.message');
|
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 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 ||
|
this.action.params.default_active_id ||
|
||||||
'channel_inbox';
|
'channel_inbox';
|
||||||
this.searchview = new SearchView(this, dataset, view_id, {}, options);
|
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', this, this.on_redirect);
|
||||||
this.thread.on('redirect_to_channel', this, function (channel_id) {
|
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('load_more_messages', this, this.load_more_messages);
|
||||||
this.thread.on('mark_as_read', this, function (message_id) {
|
this.thread.on('mark_as_read', this, function (message_id) {
|
||||||
|
|||||||
@@ -12,6 +12,296 @@ var Widget = require('web.Widget');
|
|||||||
var QWeb = core.qweb;
|
var QWeb = core.qweb;
|
||||||
var _t = core._t;
|
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<matches.length; i++) {
|
||||||
|
if (cursor_position <= matches[i].index) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
};
|
||||||
|
|
||||||
|
// add the selected suggestion to the list
|
||||||
|
var selection = this.active_listener.selection;
|
||||||
|
if (selection.length && this.active_listener.sort_selection) {
|
||||||
|
// get mention matches (ordered by index in the text)
|
||||||
|
var matches = this._get_match(text_input, this.active_listener);
|
||||||
|
var index = get_mention_index(matches, this._get_selection_positions().start);
|
||||||
|
selection.splice(index, 0, selected_suggestion);
|
||||||
|
} else {
|
||||||
|
selection = selection.concat([selected_suggestion]);
|
||||||
|
}
|
||||||
|
this._update_listener_selection(this.active_listener, selection);
|
||||||
|
|
||||||
|
// 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.active_listener.delimiter + selected_suggestion.name + ' ' + text_right;
|
||||||
|
this.composer.$input.val(text_input_new);
|
||||||
|
this._set_cursor_position(text_left.length+selected_suggestion.name.length+2);
|
||||||
|
this.set('mention_suggestions', []);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Public API
|
||||||
|
/**
|
||||||
|
* Registers a new listener, described by an object containing the following keys
|
||||||
|
* @param {char} [delimiter] the mention delimiter
|
||||||
|
* @param {function} [fetch_callback] the callback to fetch mention suggestions
|
||||||
|
* @param {string} [model] the model used for redirection
|
||||||
|
* @param {boolean} [notify_selection_changed] true to trigger an event when the selection changes
|
||||||
|
* @param {string} [redirect_classname] the classname of the <a> 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<this.listeners.length; i++) {
|
||||||
|
var listener = this.listeners[i];
|
||||||
|
this.mention_word = validate_keyword(listener.delimiter);
|
||||||
|
|
||||||
|
if (this.mention_word !== false) {
|
||||||
|
this.active_listener = listener;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.active_listener) {
|
||||||
|
// start a timeout to fetch data with the current 'mention word'. The timer avoid to start
|
||||||
|
// an RPC for each pushed key when the user is still typing.
|
||||||
|
// The '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 () {
|
||||||
|
$.when(listener.fetch_callback(self.mention_word, self.active_listener)).then(function (suggestions) {
|
||||||
|
self.set('mention_suggestions', suggestions);
|
||||||
|
});
|
||||||
|
}, this.options.typing_speed);
|
||||||
|
} else {
|
||||||
|
this.set('mention_suggestions', []); // close the dropdown
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a listener's selection should be updated after DELETE or BACKSPACE keypress
|
||||||
|
*/
|
||||||
|
check_remove: function () {
|
||||||
|
var self = this;
|
||||||
|
var to_remove = [];
|
||||||
|
var selection = this._get_selection_positions();
|
||||||
|
var deleted_binf = selection.start;
|
||||||
|
var deleted_bsup = selection.end;
|
||||||
|
|
||||||
|
_.each(this.listeners, function (listener) {
|
||||||
|
var mention_selection = listener.selection;
|
||||||
|
var matches = self._get_match(self.composer.$input.val(), listener);
|
||||||
|
for (var i=0; i<matches.length; i++) {
|
||||||
|
var m = matches[i];
|
||||||
|
var m1 = m.index;
|
||||||
|
var m2 = m.index + m[0].length;
|
||||||
|
if (deleted_binf <= m2 && m1 < deleted_bsup) {
|
||||||
|
to_remove.push(mention_selection[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (to_remove.length) {
|
||||||
|
self._update_listener_selection(listener, _.difference(mention_selection, to_remove));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces mentions appearing in the string 's' by html links with proper redirection
|
||||||
|
*/
|
||||||
|
generate_links: function (s) {
|
||||||
|
var self = this;
|
||||||
|
var mention_link = "<a href='#' class='%s' data-oe-id='%s' data-oe-model='%s'>%s%s</a>";
|
||||||
|
_.each(this.listeners, function (listener) {
|
||||||
|
var selection = listener.selection;
|
||||||
|
if (selection.length) {
|
||||||
|
var matches = self._get_match(s, listener);
|
||||||
|
var substrings = [];
|
||||||
|
var start_index = 0;
|
||||||
|
for (var i=0; i<matches.length; i++) {
|
||||||
|
var match = matches[i];
|
||||||
|
var end_index = match.index + match[0].length;
|
||||||
|
var match_name = match[0].substring(1);
|
||||||
|
var processed_text = _.str.sprintf(mention_link, listener.redirect_classname, selection[i].id, listener.model, listener.delimiter, match_name);
|
||||||
|
var subtext = s.substring(start_index, end_index).replace(match[0], processed_text);
|
||||||
|
substrings.push(subtext);
|
||||||
|
start_index = end_index;
|
||||||
|
}
|
||||||
|
substrings.push(s.substring(start_index, s.length));
|
||||||
|
s = substrings.join('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return s;
|
||||||
|
},
|
||||||
|
|
||||||
|
reset_selections: function () {
|
||||||
|
var self = this;
|
||||||
|
_.each(this.listeners, function (listener) {
|
||||||
|
self._update_listener_selection(listener, []);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Private functions
|
||||||
|
/**
|
||||||
|
* Returns the matches (as RexExp.exec does) for the mention in the input text
|
||||||
|
* @param {String} input_text: the text to search matches
|
||||||
|
* @param {Object} listener: the listener for which we want to find a match
|
||||||
|
* @returns {Object[]} matches in the same format as RexExp.exec()
|
||||||
|
*/
|
||||||
|
_get_match: function (input_text, listener) {
|
||||||
|
// create the regex of all mention's names
|
||||||
|
var names = _.pluck(listener.selection, 'name');
|
||||||
|
var escaped_names = _.map(names, function (str) {
|
||||||
|
return "("+_.str.escapeRegExp(listener.delimiter+str)+")";
|
||||||
|
});
|
||||||
|
var regex_str = escaped_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;
|
||||||
|
},
|
||||||
|
_render_suggestions: function () {
|
||||||
|
if (_.flatten(this.get('mention_suggestions')).length) {
|
||||||
|
this.$el.html(QWeb.render(this.active_listener.suggestions_template, {
|
||||||
|
suggestions: this.get('mention_suggestions'),
|
||||||
|
}));
|
||||||
|
this.$el
|
||||||
|
.addClass('open')
|
||||||
|
.find('.o_mention_proposition').first().addClass('active');
|
||||||
|
this.open = true;
|
||||||
|
} else {
|
||||||
|
this.$el.removeClass('open');
|
||||||
|
this.$el.empty();
|
||||||
|
this.open = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_update_listener_selection: function (listener, selection) {
|
||||||
|
listener.selection = selection;
|
||||||
|
if (listener.notify_selection_changed) {
|
||||||
|
this.trigger('selection_changed', listener);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Cursor position and selection utils
|
||||||
|
_get_selection_positions: function () {
|
||||||
|
var el = this.composer.$input.get(0);
|
||||||
|
return el ? {start: el.selectionStart, end: el.selectionEnd} : {start: 0, end: 0};
|
||||||
|
},
|
||||||
|
_set_cursor_position: function (pos) {
|
||||||
|
this.composer.$input.each(function (index, elem) {
|
||||||
|
if (elem.setSelectionRange){
|
||||||
|
elem.setSelectionRange(pos, pos);
|
||||||
|
}
|
||||||
|
else if (elem.createTextRange){
|
||||||
|
elem.createTextRange().collapse(true).moveEnd('character', pos).moveStart('character', pos).select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
var Composer = Widget.extend({
|
var Composer = Widget.extend({
|
||||||
template: "mail.ChatComposer",
|
template: "mail.ChatComposer",
|
||||||
|
|
||||||
@@ -22,8 +312,6 @@ var Composer = Widget.extend({
|
|||||||
"click .o_composer_button_send": "send_message",
|
"click .o_composer_button_send": "send_message",
|
||||||
"click .o_composer_button_add_attachment": "on_click_add_attachment",
|
"click .o_composer_button_add_attachment": "on_click_add_attachment",
|
||||||
"click .o_attachment_delete": "on_attachment_delete",
|
"click .o_attachment_delete": "on_attachment_delete",
|
||||||
"click .o_mention_proposition": "on_click_mention_item",
|
|
||||||
"mouseover .o_mention_proposition": "on_hover_mention_proposition",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
init: function (parent, options) {
|
init: function (parent, options) {
|
||||||
@@ -33,9 +321,6 @@ var Composer = Widget.extend({
|
|||||||
input_baseline: 18,
|
input_baseline: 18,
|
||||||
input_max_height: 150,
|
input_max_height: 150,
|
||||||
input_min_height: 28,
|
input_min_height: 28,
|
||||||
mention_delimiter: '@',
|
|
||||||
mention_min_length: 0,
|
|
||||||
mention_typing_speed: 200,
|
|
||||||
mention_fetch_limit: 8,
|
mention_fetch_limit: 8,
|
||||||
get_channel_info: function () {},
|
get_channel_info: function () {},
|
||||||
});
|
});
|
||||||
@@ -48,10 +333,26 @@ var Composer = Widget.extend({
|
|||||||
this.set('attachment_ids', []);
|
this.set('attachment_ids', []);
|
||||||
|
|
||||||
// Mention
|
// Mention
|
||||||
|
this.mention_manager = new MentionManager(this);
|
||||||
|
this.mention_manager.register({
|
||||||
|
delimiter: '@',
|
||||||
|
fetch_callback: this.mention_fetch_partners.bind(this),
|
||||||
|
model: 'res.partner',
|
||||||
|
notify_selection_changed: true,
|
||||||
|
redirect_classname: 'o_mail_redirect',
|
||||||
|
sort_selection: true,
|
||||||
|
suggestions_template: 'mail.ChatComposer.MentionPartnerSuggestions',
|
||||||
|
});
|
||||||
|
this.mention_manager.register({
|
||||||
|
delimiter: '#',
|
||||||
|
fetch_callback: this.mention_fetch_channels.bind(this),
|
||||||
|
model: 'mail.channel',
|
||||||
|
redirect_classname: 'o_channel_redirect',
|
||||||
|
suggestions_template: 'mail.ChatComposer.MentionChannelSuggestions'
|
||||||
|
});
|
||||||
|
this.mention_manager.on('selection_changed', this, this.mention_render_selection);
|
||||||
this.PartnerModel = new Model('res.partner');
|
this.PartnerModel = new Model('res.partner');
|
||||||
this.mention_dropdown_open = false;
|
this.ChannelModel = new Model('mail.channel');
|
||||||
this.set('mention_partners', []); // proposition of not-mention partner matching the mention_word
|
|
||||||
this.set('mention_selected_partners', []); // contains the mention partners sorted as they appear in the input text
|
|
||||||
},
|
},
|
||||||
|
|
||||||
start: function () {
|
start: function () {
|
||||||
@@ -60,7 +361,6 @@ var Composer = Widget.extend({
|
|||||||
this.$attachment_button = this.$(".o_composer_button_add_attachment");
|
this.$attachment_button = this.$(".o_composer_button_add_attachment");
|
||||||
this.$attachments_list = this.$('.o_composer_attachments_list');
|
this.$attachments_list = this.$('.o_composer_attachments_list');
|
||||||
this.$mention_partner_tags = this.$('.o_composer_mentioned_partners');
|
this.$mention_partner_tags = this.$('.o_composer_mentioned_partners');
|
||||||
this.$mention_dropdown = this.$('.o_composer_mention_dropdown');
|
|
||||||
this.$input = this.$('.o_composer_input');
|
this.$input = this.$('.o_composer_input');
|
||||||
this.resize_input();
|
this.resize_input();
|
||||||
|
|
||||||
@@ -86,8 +386,8 @@ var Composer = Widget.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Mention
|
// Mention
|
||||||
this.on('change:mention_partners', this, this.render_mention_partners);
|
this.mention_manager.prependTo(this.$('.o_composer'));
|
||||||
this.on('change:mention_selected_partners', this, this.render_mention_selected_partners);
|
|
||||||
return this._super();
|
return this._super();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -96,7 +396,7 @@ var Composer = Widget.extend({
|
|||||||
// behavior for the chatter composer
|
// behavior for the chatter composer
|
||||||
var value = this.$input.val().replace(/\n|\r/g, '<br/>');
|
var value = this.$input.val().replace(/\n|\r/g, '<br/>');
|
||||||
return $.when({
|
return $.when({
|
||||||
content: this.mention_preprocess_message(value),
|
content: this.mention_manager.generate_links(value),
|
||||||
attachment_ids: _.pluck(this.get('attachment_ids'), 'id'),
|
attachment_ids: _.pluck(this.get('attachment_ids'), 'id'),
|
||||||
partner_ids: _.pluck(this.get('mention_selected_partners'), 'id'),
|
partner_ids: _.pluck(this.get('mention_selected_partners'), 'id'),
|
||||||
});
|
});
|
||||||
@@ -114,7 +414,7 @@ var Composer = Widget.extend({
|
|||||||
// Empty input, selected partners and attachments
|
// Empty input, selected partners and attachments
|
||||||
self.$input.val('');
|
self.$input.val('');
|
||||||
self.resize_input();
|
self.resize_input();
|
||||||
self.set('mention_selected_partners', []);
|
self.mention_manager.reset_selections();
|
||||||
self.set('attachment_ids', []);
|
self.set('attachment_ids', []);
|
||||||
|
|
||||||
self.$input.focus();
|
self.$input.focus();
|
||||||
@@ -157,18 +457,18 @@ var Composer = Widget.extend({
|
|||||||
// UP, DOWN: prevent moving cursor if navigation in mention propositions
|
// UP, DOWN: prevent moving cursor if navigation in mention propositions
|
||||||
case $.ui.keyCode.UP:
|
case $.ui.keyCode.UP:
|
||||||
case $.ui.keyCode.DOWN:
|
case $.ui.keyCode.DOWN:
|
||||||
if (this.mention_dropdown_open) {
|
if (this.mention_manager.is_open()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
// BACKSPACE, DELETE: check if need to remove a mention
|
// BACKSPACE, DELETE: check if need to remove a mention
|
||||||
case $.ui.keyCode.BACKSPACE:
|
case $.ui.keyCode.BACKSPACE:
|
||||||
case $.ui.keyCode.DELETE:
|
case $.ui.keyCode.DELETE:
|
||||||
this.mention_check_remove();
|
this.mention_manager.check_remove();
|
||||||
break;
|
break;
|
||||||
// ENTER: submit the message only if the dropdown mention proposition is not displayed
|
// ENTER: submit the message only if the dropdown mention proposition is not displayed
|
||||||
case $.ui.keyCode.ENTER:
|
case $.ui.keyCode.ENTER:
|
||||||
if (this.mention_dropdown_open) {
|
if (this.mention_manager.is_open()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (!this.prevent_send(event)) {
|
} else if (!this.prevent_send(event)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -195,16 +495,11 @@ var Composer = Widget.extend({
|
|||||||
case $.ui.keyCode.ENTER:
|
case $.ui.keyCode.ENTER:
|
||||||
case $.ui.keyCode.UP:
|
case $.ui.keyCode.UP:
|
||||||
case $.ui.keyCode.DOWN:
|
case $.ui.keyCode.DOWN:
|
||||||
this.mention_proposition_navigation(event.which);
|
this.mention_manager.proposition_navigation(event.which);
|
||||||
break;
|
break;
|
||||||
// Otherwise, check if a mention is typed
|
// Otherwise, check if a mention is typed
|
||||||
default:
|
default:
|
||||||
this.mention_word = this.mention_detect_delimiter();
|
this.mention_manager.detect_delimiter();
|
||||||
if (this.mention_word !== false) {
|
|
||||||
this.mention_word_changed();
|
|
||||||
} else {
|
|
||||||
this.set('mention_partners', []); // close the dropdown
|
|
||||||
}
|
|
||||||
this.resize_input();
|
this.resize_input();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -294,226 +589,35 @@ var Composer = Widget.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Mention
|
// Mention
|
||||||
on_click_mention_item: function (event) {
|
mention_fetch_channels: function (search) {
|
||||||
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;
|
|
||||||
var kwargs = {
|
var kwargs = {
|
||||||
channel: this.get_channel_info(),
|
|
||||||
exclude: _.pluck(this.get('mention_selected_partners'), 'id'),
|
|
||||||
limit: this.options.mention_fetch_limit,
|
limit: this.options.mention_fetch_limit,
|
||||||
search: search,
|
search: search,
|
||||||
};
|
};
|
||||||
this.PartnerModel.call('get_mention_suggestions', kwargs).then(function (suggestions) {
|
return this.ChannelModel
|
||||||
self.set('mention_partners', suggestions);
|
.call('get_mention_suggestions', kwargs)
|
||||||
});
|
.then(function (suggestions) {
|
||||||
|
return _.partition(suggestions, function (suggestion) {
|
||||||
|
return _.contains(['public', 'groups'], suggestion.public);
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
mention_fetch_partners: function (search, listener) {
|
||||||
mention_check_remove: function () {
|
var kwargs = {
|
||||||
var mention_selected_partners = this.get('mention_selected_partners');
|
channel: this.get_channel_info(),
|
||||||
var partners_to_remove = [];
|
exclude: listener ? _.pluck(listener.selection, 'id') : [],
|
||||||
var selection = this.get_selection_positions();
|
limit: this.options.mention_fetch_limit,
|
||||||
var deleted_binf = selection.start;
|
search: search,
|
||||||
var deleted_bsup = selection.end;
|
};
|
||||||
|
return this.PartnerModel.call('get_mention_suggestions', kwargs);
|
||||||
var matches = this.mention_get_match(this.$input.val());
|
|
||||||
for (var i=0; i<matches.length; i++) {
|
|
||||||
var m = matches[i];
|
|
||||||
var m1 = m.index;
|
|
||||||
var m2 = m.index + m[0].length;
|
|
||||||
if (deleted_binf <= m2 && m1 < deleted_bsup) {
|
|
||||||
partners_to_remove.push(mention_selected_partners[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.set('mention_selected_partners', _.difference(mention_selected_partners, partners_to_remove));
|
|
||||||
},
|
},
|
||||||
|
mention_render_selection: function (listener) {
|
||||||
mention_preprocess_message: function (message) {
|
|
||||||
var partners = this.get('mention_selected_partners');
|
|
||||||
if (partners.length) {
|
|
||||||
var matches = this.mention_get_match(message);
|
|
||||||
var substrings = [];
|
|
||||||
var start_index = 0;
|
|
||||||
for (var i=0; i<matches.length; i++) {
|
|
||||||
var match = matches[i];
|
|
||||||
var end_index = match.index + match[0].length;
|
|
||||||
var partner_name = match[0].substring(1);
|
|
||||||
var processed_text = _.str.sprintf("<a href='#' class='o_mail_redirect' data-oe-model='res.partner' data-oe-id='%s'>@%s</a>", 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 () {
|
|
||||||
this.$mention_partner_tags.html(QWeb.render('mail.ChatComposer.MentionTags', {
|
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<matches.length; i++) {
|
|
||||||
if (cursor_position <= matches[i].index) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return i;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Others
|
// Others
|
||||||
get_selection_positions: function () {
|
|
||||||
var el = this.$input.get(0);
|
|
||||||
return el ? {start: el.selectionStart, end: el.selectionEnd} : {start: 0, end: 0};
|
|
||||||
},
|
|
||||||
|
|
||||||
set_cursor_position: function (pos) {
|
|
||||||
this.$input.each(function (index, elem) {
|
|
||||||
if (elem.setSelectionRange){
|
|
||||||
elem.setSelectionRange(pos, pos);
|
|
||||||
}
|
|
||||||
else if (elem.createTextRange){
|
|
||||||
elem.createTextRange().collapse(true).moveEnd('character', pos).moveStart('character', pos).select();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
is_empty: function () {
|
is_empty: function () {
|
||||||
return !this.$input.val().trim() && !this.$('.o_attachments').children().length;
|
return !this.$input.val().trim() && !this.$('.o_attachments').children().length;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ odoo.define('mail.systray', function (require) {
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var core = require('web.core');
|
var core = require('web.core');
|
||||||
var Model = require('web.Model');
|
|
||||||
var SystrayMenu = require('web.SystrayMenu');
|
var SystrayMenu = require('web.SystrayMenu');
|
||||||
var web_client = require('web.web_client');
|
var web_client = require('web.web_client');
|
||||||
var Widget = require('web.Widget');
|
var Widget = require('web.Widget');
|
||||||
@@ -20,16 +19,6 @@ var NotificationTopButton = Widget.extend({
|
|||||||
events: {
|
events: {
|
||||||
"click": "on_click",
|
"click": "on_click",
|
||||||
},
|
},
|
||||||
willStart: function () {
|
|
||||||
var self = this;
|
|
||||||
var ir_model = new Model("ir.model.data");
|
|
||||||
var def1 = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_menu_root_chat"]);
|
|
||||||
var def2 = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_action_client_chat"]);
|
|
||||||
return $.when(def1, def2, this._super()).then(function (menu_id, action_id) {
|
|
||||||
self.discuss_menu_id = menu_id;
|
|
||||||
self.client_action_id = action_id;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
start: function () {
|
start: function () {
|
||||||
chat_manager.bus.on("update_needaction", this, this.update_counter);
|
chat_manager.bus.on("update_needaction", this, this.update_counter);
|
||||||
this.update_counter(chat_manager.get_needaction_counter());
|
this.update_counter(chat_manager.get_needaction_counter());
|
||||||
@@ -39,10 +28,10 @@ var NotificationTopButton = Widget.extend({
|
|||||||
this.$('.o_notification_counter').html(counter);
|
this.$('.o_notification_counter').html(counter);
|
||||||
},
|
},
|
||||||
on_click: function (event) {
|
on_click: function (event) {
|
||||||
var self = this;
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.do_action(this.client_action_id, {clear_breadcrumbs: true}).then(function () {
|
var discuss_ids = chat_manager.get_discuss_ids();
|
||||||
core.bus.trigger('change_menu_section', self.discuss_menu_id);
|
this.do_action(discuss_ids.action_id, {clear_breadcrumbs: true}).then(function () {
|
||||||
|
core.bus.trigger('change_menu_section', discuss_ids.menu_id);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ var Thread = Widget.extend({
|
|||||||
|
|
||||||
on_channel_redirect: function (event) {
|
on_channel_redirect: function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
var channel_id = $(event.target).data('channel-id');
|
var channel_id = $(event.target).data('oe-id');
|
||||||
this.trigger('redirect_to_channel', channel_id);
|
this.trigger('redirect_to_channel', channel_id);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
<div t-name="mail.ChatComposer" class="o_chat_composer">
|
<div t-name="mail.ChatComposer" class="o_chat_composer">
|
||||||
<div class="o_composer">
|
<div class="o_composer">
|
||||||
<div class="dropup o_composer_mention_dropdown"/>
|
|
||||||
<textarea class="o_composer_input" placeholder="Write something"/>
|
<textarea class="o_composer_input" placeholder="Write something"/>
|
||||||
<div class="btn-group o_composer_buttons">
|
<div class="btn-group o_composer_buttons">
|
||||||
<button class="btn btn-sm btn-primary o_composer_button_send" type="button">Send</button>
|
<button class="btn btn-sm btn-primary o_composer_button_send" type="button">Send</button>
|
||||||
@@ -34,14 +33,14 @@
|
|||||||
</t>
|
</t>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
<t t-name="mail.ChatComposer.MentionMenu">
|
<t t-name="mail.ChatComposer.MentionPartnerSuggestions">
|
||||||
<ul class="dropdown-menu" aria-labelledby="dropdownMailMentionMenu">
|
<ul class="dropdown-menu" aria-labelledby="dropdownMailMentionMenu">
|
||||||
<t t-set="show_divider" t-value="false"/>
|
<t t-set="show_divider" t-value="false"/>
|
||||||
<t t-foreach="suggestions" t-as="group">
|
<t t-foreach="suggestions" t-as="group">
|
||||||
<t t-foreach="group" t-as="partner">
|
<t t-foreach="group" t-as="partner">
|
||||||
<li t-if="show_divider && (partner_index === 0)" class="divider"/>
|
<li t-if="show_divider && (partner_index === 0)" class="divider"/>
|
||||||
<t t-set="show_divider" t-value="true"/>
|
<t t-set="show_divider" t-value="true"/>
|
||||||
<li class="o_mention_proposition" t-att-data-partner-id="partner.id">
|
<li class="o_mention_proposition" t-att-data-id="partner.id">
|
||||||
<a href="#">
|
<a href="#">
|
||||||
<t t-esc="partner.name"/> <t t-if="partner.email">(<t t-esc="partner.email"/>)</t>
|
<t t-esc="partner.name"/> <t t-if="partner.email">(<t t-esc="partner.email"/>)</t>
|
||||||
</a>
|
</a>
|
||||||
@@ -51,6 +50,21 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
|
<t t-name="mail.ChatComposer.MentionChannelSuggestions">
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="dropdownMailMentionMenu">
|
||||||
|
<t t-set="show_divider" t-value="false"/>
|
||||||
|
<t t-foreach="suggestions" t-as="group">
|
||||||
|
<t t-foreach="group" t-as="channel">
|
||||||
|
<li t-if="show_divider && (channel_index === 0)" class="divider"/>
|
||||||
|
<t t-set="show_divider" t-value="true"/>
|
||||||
|
<li class="o_mention_proposition" t-att-data-id="channel.id">
|
||||||
|
<a href="#"><t t-esc="channel.name"/></a>
|
||||||
|
</li>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</t>
|
||||||
|
|
||||||
<t t-name="mail.ChatComposer.MentionTags">
|
<t t-name="mail.ChatComposer.MentionTags">
|
||||||
<t t-if="partners.length">Mention(s): </t>
|
<t t-if="partners.length">Mention(s): </t>
|
||||||
<t t-foreach="partners" t-as="partner">
|
<t t-foreach="partners" t-as="partner">
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
on <a class="o_mail_redirect" href="#" t-att-data-oe-model="message.model" t-att-data-oe-id="message.res_id"><t t-esc="message.record_name"/></a>
|
on <a class="o_mail_redirect" href="#" t-att-data-oe-model="message.model" t-att-data-oe-id="message.res_id"><t t-esc="message.record_name"/></a>
|
||||||
</t>
|
</t>
|
||||||
<t t-if="message.origin_id && (message.origin_id !== options.channel_id)">
|
<t t-if="message.origin_id && (message.origin_id !== options.channel_id)">
|
||||||
(in channel <a class="o_channel_redirect" t-att-data-channel-id="message.origin_id" href="#"><t t-esc="message.origin_name"/>)</a>
|
(in channel <a class="o_channel_redirect" t-att-data-oe-id="message.origin_id" href="#"><t t-esc="message.origin_name"/>)</a>
|
||||||
</t>
|
</t>
|
||||||
</p>
|
</p>
|
||||||
<t t-if="message.tracking_value_ids && message.tracking_value_ids.length > 0">
|
<t t-if="message.tracking_value_ids && message.tracking_value_ids.length > 0">
|
||||||
|
|||||||
Reference in New Issue
Block a user