[REF] mail: first part of the chat refactoring (client side)
Joint work with ged
This commit is contained in:
committed by
Géry Debongnie
parent
aa23a5a41d
commit
512287cac1
@@ -0,0 +1,442 @@
|
||||
odoo.define('mail.chat_manager', function (require) {
|
||||
"use strict";
|
||||
|
||||
var bus = require('bus.bus').bus;
|
||||
var core = require('web.core');
|
||||
var data = require('web.data');
|
||||
var Model = require('web.Model');
|
||||
var session = require('web.session');
|
||||
var web_client = require('web.web_client');
|
||||
|
||||
var _t = core._t;
|
||||
var LIMIT = 20;
|
||||
|
||||
var MessageModel = new Model('mail.message', session.context);
|
||||
var ChannelModel = new Model('mail.channel', session.context);
|
||||
|
||||
// Private model
|
||||
//----------------------------------------------------------------------------------
|
||||
var messages = [];
|
||||
var channels = [];
|
||||
var emojis = [];
|
||||
var emoji_substitutions = {};
|
||||
var needaction_counter = 0;
|
||||
|
||||
// Message and channel manipulation helpers
|
||||
//----------------------------------------------------------------------------------
|
||||
|
||||
// options: channel_id, silent
|
||||
function add_message (data, options) {
|
||||
options = options || {};
|
||||
var msg = _.findWhere(messages, { id: data.id });
|
||||
|
||||
if (!msg) {
|
||||
msg = make_message(data, options.channel_id);
|
||||
// Keep the array ordered by date when inserting the new message
|
||||
messages.splice(_.sortedIndex(messages, msg, 'date'), 0, msg);
|
||||
if (options.channel_id) {
|
||||
var channel = _.findWhere(channels, {id: options.channel_id});
|
||||
if (channel.hidden) {
|
||||
channel.hidden = false;
|
||||
chat_manager.bus.trigger('new_channel', channel);
|
||||
}
|
||||
if ((channel.type === "dm") && (options.show_notification)) {
|
||||
var query = { is_displayed: false };
|
||||
chat_manager.bus.trigger('anyone_listening', channel, query);
|
||||
if (!query.is_displayed) {
|
||||
web_client.do_notify(_t('New message'), _t("You received a message from ") + msg.author_id[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!options.silent) {
|
||||
chat_manager.bus.trigger('new_message', msg);
|
||||
}
|
||||
} else if (msg && options.channel_id !== undefined) {
|
||||
add_channel_to_message(msg, options.channel_id);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
function make_message (data, channel_id) {
|
||||
var msg = {
|
||||
id: data.id,
|
||||
author_id: data.author_id,
|
||||
body: data.body,
|
||||
date: data.date,
|
||||
message_type: data.message_type,
|
||||
is_note: data.is_note,
|
||||
attachment_ids: data.attachment_ids,
|
||||
subject: data.subject,
|
||||
email_from: data.email_from,
|
||||
record_name: data.record_name,
|
||||
tracking_value_ids: data.tracking_value_ids,
|
||||
channel_ids: (channel_id !== undefined ? [channel_id] : []),
|
||||
model: data.model,
|
||||
res_id: data.res_id,
|
||||
};
|
||||
|
||||
_.each(_.keys(emoji_substitutions), function(key){
|
||||
var escaped_key = String(key).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1'); //]
|
||||
var regexp = new RegExp("(?:^|\\s|<[a-z]*>)(" + escaped_key + ")(?:\\s|$|</[a-z]*>)");
|
||||
msg.body = msg.body.replace(regexp, ' <span class="o_mail_emoji">'+emoji_substitutions[key]+'</span> ');
|
||||
});
|
||||
|
||||
function property_descr(channel) {
|
||||
return {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _.contains(msg.channel_ids, channel);
|
||||
},
|
||||
set: function (bool) {
|
||||
if (bool) {
|
||||
add_channel_to_message(msg, channel);
|
||||
} else {
|
||||
msg.channel_ids = _.without(msg.channel_ids, channel);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Object.defineProperties(msg, {
|
||||
is_starred: property_descr("channel_starred"),
|
||||
is_needaction: property_descr("channel_inbox"),
|
||||
});
|
||||
|
||||
if (_.contains(data.needaction_partner_ids, session.partner_id)) {
|
||||
msg.is_needaction = true;
|
||||
}
|
||||
if (_.contains(data.starred_partner_ids, session.partner_id)) {
|
||||
msg.is_starred = true;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
function add_channel_to_message (message, channel_id) {
|
||||
message.channel_ids.push(channel_id);
|
||||
message.channel_ids = _.uniq(message.channel_ids);
|
||||
}
|
||||
|
||||
function post_channel_message (data) {
|
||||
return ChannelModel.call('message_post', [data.channel_id], {
|
||||
message_type: 'comment',
|
||||
content_subtype: 'html',
|
||||
partner_ids: data.partner_ids,
|
||||
body: data.content,
|
||||
subtype: 'mail.mt_comment',
|
||||
attachment_ids: data.attachment_ids,
|
||||
});
|
||||
}
|
||||
|
||||
function post_document_message (model_name, res_id, data) {
|
||||
var values = {
|
||||
attachment_ids: data.attachment_ids,
|
||||
body: data.content,
|
||||
content_subtype: data.content_subtype,
|
||||
message_type: data.message_type,
|
||||
partner_ids: data.partner_ids,
|
||||
subtype: data.subtype,
|
||||
subtype_id: data.subtype_id,
|
||||
};
|
||||
|
||||
var model = new Model(model_name);
|
||||
return model.call('message_post', [res_id], values).then(function (msg_id) {
|
||||
return MessageModel.call('message_format', [msg_id]).then(function (msgs) {
|
||||
msgs[0].model = model_name;
|
||||
msgs[0].res_id = res_id;
|
||||
add_message(msgs[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function add_channel (data, options) {
|
||||
options = typeof options === "object" ? options : {};
|
||||
var channel = _.findWhere(channels, {id: data.id});
|
||||
if (channel) {
|
||||
if (channel.is_folded !== (data.state === "folded")) {
|
||||
channel.is_folded = (data.state === "folded");
|
||||
chat_manager.bus.trigger("channel_toggle_fold", channel);
|
||||
}
|
||||
} else {
|
||||
channel = make_channel(data, options);
|
||||
channels.push(channel);
|
||||
if (!options.silent) {
|
||||
chat_manager.bus.trigger("new_channel", channel);
|
||||
}
|
||||
if (channel.is_detached) {
|
||||
chat_manager.bus.trigger("open_chat", channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function make_channel (data, options) {
|
||||
var channel = {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
type: data.type || "public",
|
||||
all_history_loaded: false,
|
||||
uuid: data.uuid,
|
||||
is_detached: data.is_minimized,
|
||||
is_folded: data.state === "folded",
|
||||
loaded: false,
|
||||
autoswitch: 'autoswitch' in options ? options.autoswitch : true,
|
||||
hidden: options.hidden,
|
||||
};
|
||||
if (data.public === "private") {
|
||||
channel.type = "private";
|
||||
}
|
||||
if ('direct_partner' in data) {
|
||||
channel.type = "dm";
|
||||
channel.name = data.direct_partner[0].name;
|
||||
channel.status = data.direct_partner[0].im_status;
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
||||
// options: domain, load_more
|
||||
function fetch_from_channel (channel_id, options) {
|
||||
var domain =
|
||||
(channel_id === "channel_inbox") ? [['needaction', '=', true]] :
|
||||
(channel_id === "channel_starred") ? [['starred', '=', true]] :
|
||||
[['channel_ids', 'in', channel_id]];
|
||||
|
||||
options = options || {};
|
||||
if (options.domain) {
|
||||
domain = new data.CompoundDomain(domain, options.domain || []);
|
||||
}
|
||||
if (options.load_more) {
|
||||
var min_message_id = _.chain(messages)
|
||||
.filter(function (msg) { return _.contains(msg.channel_ids, channel_id); })
|
||||
.pluck("id")
|
||||
.min()
|
||||
.value();
|
||||
|
||||
domain = new data.CompoundDomain([['id', '<', min_message_id]], domain);
|
||||
}
|
||||
|
||||
return MessageModel.call('message_fetch', [domain], {limit: LIMIT}).then(function (msgs) {
|
||||
var channel = _.findWhere(channels, {id: channel_id});
|
||||
channel.loaded = true;
|
||||
if (!channel.all_history_loaded) {
|
||||
channel.all_history_loaded = msgs.length < LIMIT;
|
||||
}
|
||||
|
||||
_.each(msgs, function (msg) {
|
||||
add_message(msg, {channel_id: channel_id, silent: true});
|
||||
});
|
||||
return _.filter(messages, function (m) {
|
||||
return _.contains(m.channel_ids, channel_id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// options: force_fetch
|
||||
function fetch_document_messages (ids, options) {
|
||||
var loaded_msgs = _.filter(messages, function (message) {
|
||||
return _.contains(ids, message.id);
|
||||
});
|
||||
var loaded_msg_ids = _.pluck(loaded_msgs, 'id');
|
||||
|
||||
options = options || {};
|
||||
if (options.force_fetch || _.difference(ids.slice(0, LIMIT), loaded_msg_ids).length) {
|
||||
var ids_to_load = _.difference(ids, loaded_msg_ids).slice(0, LIMIT);
|
||||
|
||||
return MessageModel.call('message_format', [ids_to_load]).then(function (msgs) {
|
||||
var processed_msgs = [];
|
||||
_.each(msgs, function (msg) {
|
||||
processed_msgs.push(add_message(msg, {silent: true}));
|
||||
});
|
||||
return _.sortBy(loaded_msgs.concat(processed_msgs), function (msg) {
|
||||
return msg.date;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return $.when(loaded_msgs);
|
||||
}
|
||||
}
|
||||
|
||||
// Public interface
|
||||
//----------------------------------------------------------------------------------
|
||||
var chat_manager = {
|
||||
post_message: post_channel_message,
|
||||
post_message_in_document: post_document_message,
|
||||
|
||||
get_messages: function (options) {
|
||||
if ('channel_id' in options) { // channel message
|
||||
var channel = _.findWhere(channels, {id: options.channel_id});
|
||||
if (channel.loaded) {
|
||||
return $.when(_.filter(messages, function (message) {
|
||||
return _.contains(message.channel_ids, channel.id);
|
||||
}));
|
||||
} else {
|
||||
return fetch_from_channel(options.channel_id);
|
||||
}
|
||||
} else { // chatter message
|
||||
}
|
||||
},
|
||||
fetch: function (channel_id, domain) {
|
||||
return fetch_from_channel(channel_id, {domain: domain});
|
||||
},
|
||||
fetch_more: function (channel_id, domain) {
|
||||
return fetch_from_channel(channel_id, {domain: domain, load_more: true});
|
||||
},
|
||||
/**
|
||||
* Fetches chatter messages from their ids
|
||||
*/
|
||||
fetch_messages: function (message_ids, options) {
|
||||
return fetch_document_messages(message_ids, options);
|
||||
},
|
||||
toggle_star_status: function (message_id) {
|
||||
var msg = _.findWhere(messages, { id: message_id });
|
||||
|
||||
return MessageModel.call('set_message_starred', [[message_id], !msg.is_starred]).then(function () {
|
||||
msg.is_starred = !msg.is_starred;
|
||||
chat_manager.bus.trigger('update_message', msg);
|
||||
});
|
||||
},
|
||||
mark_as_read: function (message_id) {
|
||||
return MessageModel.call('set_message_done', [[message_id]]).then(function () {
|
||||
var message = _.findWhere(messages, { id: message_id });
|
||||
message.channel_ids = _.without(message.channel_ids, "channel_inbox");
|
||||
chat_manager.bus.trigger('update_message', message);
|
||||
});
|
||||
},
|
||||
|
||||
get_channels: function () {
|
||||
return _.clone(channels);
|
||||
},
|
||||
|
||||
get_channel: function (id) {
|
||||
return _.findWhere(channels, {id: id}) || channels[0];
|
||||
},
|
||||
|
||||
all_history_loaded: function (channel_id) {
|
||||
return this.get_channel(channel_id).all_history_loaded;
|
||||
},
|
||||
|
||||
get_emojis: function() {
|
||||
return emojis;
|
||||
},
|
||||
|
||||
get_needaction_counter: function () {
|
||||
return needaction_counter;
|
||||
},
|
||||
|
||||
detach_channel: function (channel_id) {
|
||||
var channel = this.get_channel(channel_id);
|
||||
return ChannelModel.call("channel_minimize", [channel.uuid, true]);
|
||||
},
|
||||
remove_chatter_messages: function (model) {
|
||||
messages = _.reject(messages, function (message) {
|
||||
return message.channel_ids.length === 0 && message.model === model;
|
||||
});
|
||||
},
|
||||
bus: new core.Bus(),
|
||||
|
||||
create_channel: function (name, type) {
|
||||
var method = type === "dm" ? "channel_get" : "channel_create";
|
||||
var args = type === "dm" ? [[name]] : [name, type];
|
||||
|
||||
return ChannelModel
|
||||
.call(method, args)
|
||||
.then(add_channel)
|
||||
.then(function () {
|
||||
bus.restart_poll();
|
||||
});
|
||||
},
|
||||
join_channel: function (channel_id) {
|
||||
return ChannelModel
|
||||
.call('channel_join_and_get_info', [[channel_id]])
|
||||
.then(add_channel);
|
||||
},
|
||||
|
||||
unsubscribe: function (channel_id) {
|
||||
var def;
|
||||
var channel = chat_manager.get_channel(channel_id);
|
||||
if (channel.type === "dm") {
|
||||
def = ChannelModel.call('channel_pin', [channel.uuid, false]);
|
||||
} else {
|
||||
def = ChannelModel.call('action_unfollow', [[channel_id]]);
|
||||
}
|
||||
return def.then(function () {
|
||||
channels = _.without(channels, channel);
|
||||
});
|
||||
},
|
||||
close_chat_session: function (channel_id) {
|
||||
var channel = _.findWhere(channels, {id: channel_id});
|
||||
ChannelModel.call("channel_fold", [], {uuid : channel.uuid, state : "closed"});
|
||||
},
|
||||
fold_channel: function (channel_id) {
|
||||
var channel = _.findWhere(channels, {id: channel_id});
|
||||
return ChannelModel.call("channel_fold", [], {uuid : channel.uuid}).then(function () {
|
||||
channel.is_folded = !channel.is_folded;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// Initialization
|
||||
// ---------------------------------------------------------------------------------
|
||||
function init () {
|
||||
add_channel({
|
||||
id: "channel_inbox",
|
||||
name: _t("Inbox"),
|
||||
type: "static"
|
||||
});
|
||||
|
||||
add_channel({
|
||||
id: "channel_starred",
|
||||
name: _t("Starred"),
|
||||
type: "static"
|
||||
});
|
||||
|
||||
var load_channels = session.rpc('/mail/client_action').then(function (result) {
|
||||
_.each(result.channel_slots.channel_channel, add_channel);
|
||||
_.each(result.channel_slots.channel_private_group, add_channel);
|
||||
_.each(result.channel_slots.channel_direct_message, add_channel);
|
||||
needaction_counter = result.needaction_inbox_counter;
|
||||
});
|
||||
|
||||
var load_emojis = session.rpc("/mail/chat_init").then(function (result) {
|
||||
emojis = result.emoji;
|
||||
_.each(emojis, function(emoji) {
|
||||
emoji_substitutions[emoji.source] = emoji.substitution;
|
||||
});
|
||||
});
|
||||
|
||||
bus.on('notification', null, function (notification) {
|
||||
var model = notification[0][1];
|
||||
if (model === 'mail.channel') {
|
||||
// new message in a channel
|
||||
var message = notification[1];
|
||||
var channel_id = message.channel_ids[0];
|
||||
add_message(message, { channel_id: channel_id, show_notification: true} );
|
||||
}
|
||||
if (model === 'res.partner') {
|
||||
var chat_session = notification[1];
|
||||
if ((chat_session.channel_type === "channel") && (chat_session.public === "private") && (chat_session.state === "open")) {
|
||||
add_channel(chat_session, {autoswitch: false});
|
||||
if (!chat_session.is_minimized) {
|
||||
web_client.do_notify(_t("Private Channel"), _t("You have been invited to: ") + chat_session.name);
|
||||
}
|
||||
}
|
||||
// partner specific change (open a detached window for example)
|
||||
if ((chat_session.state === "open") || (chat_session.state === "folded")) {
|
||||
add_channel(chat_session, {autoswitch: false, silent: true, hidden: true});
|
||||
if (chat_session.is_minimized) {
|
||||
chat_manager.bus.trigger("open_chat", chat_session);
|
||||
}
|
||||
}
|
||||
if (chat_session.state === "closed") {
|
||||
chat_manager.bus.trigger("close_chat", chat_session);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return $.when(load_channels, load_emojis).then(function () {
|
||||
bus.start_polling();
|
||||
return chat_manager;
|
||||
});
|
||||
}
|
||||
|
||||
return init();
|
||||
|
||||
});
|
||||
@@ -0,0 +1,897 @@
|
||||
odoo.define('mail.Chatter', function (require) {
|
||||
"use strict";
|
||||
|
||||
var chat_manager = require('mail.chat_manager');
|
||||
var ChatComposer = require('mail.ChatComposer');
|
||||
var ChatThread = require('mail.ChatThread');
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var data = require('web.data');
|
||||
var Dialog = require('web.Dialog');
|
||||
var form_common = require('web.form_common');
|
||||
var session = require('web.session');
|
||||
var web_client = require('web.web_client');
|
||||
|
||||
var _t = core._t;
|
||||
var qweb = core.qweb;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Followers Widget ('mail_followers' widget)
|
||||
//
|
||||
// Since it is displayed on a form view, it extends 'AbstractField' widget.
|
||||
//
|
||||
// Note: the followers widget is moved inside the chatter by the chatter itself
|
||||
// for layout purposes.
|
||||
// -----------------------------------------------------------------------------
|
||||
var Followers = form_common.AbstractField.extend({
|
||||
template: 'mail.Followers',
|
||||
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this.image = this.node.attrs.image || 'image_small';
|
||||
this.comment = this.node.attrs.help || false;
|
||||
this.ds_model = new data.DataSetSearch(this, this.view.model);
|
||||
this.ds_users = new data.DataSetSearch(this, 'res.users');
|
||||
|
||||
this.value = [];
|
||||
this.followers = [];
|
||||
this.data_subtype = {};
|
||||
|
||||
this.view_is_editable = this.__parentedParent.is_action_enabled('edit');
|
||||
},
|
||||
|
||||
start: function () {
|
||||
// use actual_mode property on view to know if the view is in create mode anymore
|
||||
this.view.on("change:actual_mode", this, this.on_check_visibility_mode);
|
||||
this.on_check_visibility_mode();
|
||||
this.reinit();
|
||||
this.bind_events();
|
||||
return this._super();
|
||||
},
|
||||
|
||||
on_check_visibility_mode: function () {
|
||||
this.set({"force_invisible": this.view.get("actual_mode") === "create"});
|
||||
},
|
||||
|
||||
set_value: function (_value) {
|
||||
this.value = _value;
|
||||
this._super(_value);
|
||||
},
|
||||
|
||||
reinit: function () {
|
||||
this.data_subtype = {};
|
||||
this.message_is_follower = undefined;
|
||||
this.display_buttons();
|
||||
},
|
||||
|
||||
bind_events: function () {
|
||||
var self = this;
|
||||
|
||||
// event: click on '(Un)Follow' button, that toggles the follow for uid
|
||||
this.$el.on('click', '.o_followers_follow_button', function () {
|
||||
if ($(this).hasClass('o_followers_notfollow')) {
|
||||
self.do_follow();
|
||||
} else {
|
||||
self.do_unfollow({user_ids: [session.uid]});
|
||||
}
|
||||
});
|
||||
|
||||
// event: click on a subtype, that (un)subscribe for this subtype
|
||||
this.$el.on('click', '.o_subtypes_list input', function(event) {
|
||||
event.stopPropagation();
|
||||
self.do_update_subscription(event);
|
||||
var $list = self.$('.o_subtypes_list');
|
||||
if (!$list.hasClass('open')) {
|
||||
$list.addClass('open');
|
||||
}
|
||||
if (self.$('.o_subtypes_list ul')[0].children.length < 1) {
|
||||
$list.removeClass('open');
|
||||
}
|
||||
});
|
||||
|
||||
// event: click on 'invite' button, that opens the invite wizard
|
||||
this.$el.on('click', '.o_add_follower', function(event) {
|
||||
event.preventDefault();
|
||||
self.on_invite_follower(false);
|
||||
});
|
||||
this.$el.on('click', '.o_add_follower_channel', function(event) {
|
||||
event.preventDefault();
|
||||
self.on_invite_follower(true);
|
||||
});
|
||||
|
||||
// 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);
|
||||
},
|
||||
|
||||
on_edit_subtype: function (event) {
|
||||
var self = this;
|
||||
var $currentTarget = $(event.currentTarget);
|
||||
var follower_id = $currentTarget.data('oe-id');
|
||||
var is_channel = $currentTarget.data('oe-model') === 'mail.channel';
|
||||
self.dialog = new Dialog(this, {
|
||||
size: 'small',
|
||||
title: _t('Edit Subscription of ') + $currentTarget.siblings('a').text(),
|
||||
buttons: [
|
||||
{
|
||||
text: _t("Apply"),
|
||||
classes: 'btn-primary',
|
||||
click: function () {
|
||||
self.do_update_subscription(event, follower_id, is_channel);
|
||||
},
|
||||
close: true
|
||||
},
|
||||
{
|
||||
text: _t("Cancel"),
|
||||
close: true,
|
||||
},
|
||||
],
|
||||
}).open();
|
||||
return self.fetch_subtypes($currentTarget.data('id'));
|
||||
},
|
||||
|
||||
on_invite_follower: function (channel_only) {
|
||||
var self = this;
|
||||
var action = {
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'mail.wizard.invite',
|
||||
view_mode: 'form',
|
||||
view_type: 'form',
|
||||
views: [[false, 'form']],
|
||||
name: _t('Invite Follower'),
|
||||
target: 'new',
|
||||
context: {
|
||||
'default_res_model': this.view.dataset.model,
|
||||
'default_res_id': this.view.datarecord.id,
|
||||
'mail_invite_follower_channel_only': channel_only,
|
||||
},
|
||||
};
|
||||
this.do_action(action, {
|
||||
on_close: function () {
|
||||
self.read_value();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
on_remove_follower: function (event) {
|
||||
var res_model = $(event.target).parent().find('a').data('oe-model');
|
||||
var res_id = $(event.target).parent().find('a').data('oe-id');
|
||||
if (res_model === 'res.partner') {
|
||||
return this.do_unfollow({partner_ids: [res_id]});
|
||||
} else {
|
||||
return this.do_unfollow({channel_ids: [res_id]});
|
||||
}
|
||||
},
|
||||
|
||||
on_follower_clicked: 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);
|
||||
},
|
||||
|
||||
read_value: function () {
|
||||
var self = this;
|
||||
return this.ds_model.read_ids([this.view.datarecord.id], ['message_follower_ids'])
|
||||
.then(function (results) {
|
||||
self.value = results[0].message_follower_ids;
|
||||
self.render_value();
|
||||
});
|
||||
},
|
||||
|
||||
render_value: function () {
|
||||
this.reinit();
|
||||
return this.fetch_followers(this.value);
|
||||
},
|
||||
|
||||
fetch_followers: function (value_) {
|
||||
this.value = value_ || [];
|
||||
return ajax.jsonRpc('/mail/read_followers', 'call', {'follower_ids': this.value})
|
||||
.then(this.proxy('display_followers'), this.proxy('fetch_generic'))
|
||||
.then(this.proxy('display_buttons'))
|
||||
.then(this.proxy('fetch_subtypes'));
|
||||
},
|
||||
|
||||
/** Read on res.partner failed: fall back on a generic case
|
||||
- fetch current user partner_id (call because no other smart solution currently) FIXME
|
||||
- then display a generic message about followers */
|
||||
fetch_generic: function () {
|
||||
var self = this;
|
||||
|
||||
return this.ds_users.call('read', [[session.uid], ['partner_id']])
|
||||
.then(function (results) {
|
||||
var pid = results[0].partner_id[0];
|
||||
self.message_is_follower = (_.indexOf(self.value, pid) !== -1);
|
||||
})
|
||||
.then(self.proxy('display_generic'));
|
||||
},
|
||||
|
||||
_format_followers: function (count){
|
||||
var str = '';
|
||||
if(count <= 0){
|
||||
str = _t('No follower');
|
||||
}else if(count === 1){
|
||||
str = _t('One follower');
|
||||
}else{
|
||||
str = ''+count+' '+_t('followers');
|
||||
}
|
||||
return str;
|
||||
},
|
||||
|
||||
/* Display generic info about follower, for people not having access to res_partner */
|
||||
display_generic: function () {
|
||||
this.$('.o_followers_list').empty();
|
||||
this.$('.o_followers_count').html(this._format_followers(this.value.length));
|
||||
},
|
||||
|
||||
/** Display the followers */
|
||||
display_followers: function (records) {
|
||||
var self = this;
|
||||
this.followers = records || this.followers;
|
||||
|
||||
// clean and display title
|
||||
var $followers_list = this.$('.o_followers_list').empty();
|
||||
this.$('.o_followers_count').html(this._format_followers(this.followers.length));
|
||||
var user_follower = _.filter(this.followers, function (rec) { return rec.is_uid;});
|
||||
this.message_is_follower = user_follower.length >= 1;
|
||||
this.follower_id = this.message_is_follower ? user_follower[0].id : undefined;
|
||||
|
||||
// render the dropdown content
|
||||
$(qweb.render('mail.Followers.add_more', {'widget': self})).appendTo($followers_list);
|
||||
var $follower_li;
|
||||
_(this.followers).each(function (record) {
|
||||
$follower_li = $(qweb.render('mail.Followers.partner', {
|
||||
'record': _.extend(record, {'avatar_url': '/web/image/' + record.res_model + '/' + record.res_id + '/image_small'}),
|
||||
'widget': self})
|
||||
);
|
||||
$follower_li.appendTo($followers_list);
|
||||
|
||||
// On mouse-enter it will show the edit_subtype pencil.
|
||||
if (record.is_editable) {
|
||||
$follower_li.on('mouseenter mouseleave', function(e) {
|
||||
$(e.currentTarget).find('.o_edit_subtype').toggleClass('hide', e.type === 'mouseleave');
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
display_buttons: function () {
|
||||
if (this.message_is_follower) {
|
||||
this.$('button.o_followers_follow_button').removeClass('o_followers_notfollow').addClass('o_followers_following');
|
||||
} else {
|
||||
this.$('button.o_followers_follow_button').removeClass('o_followers_following').addClass('o_followers_notfollow');
|
||||
}
|
||||
},
|
||||
|
||||
/** Fetch subtypes, only if current user is follower */
|
||||
fetch_subtypes: function (follower_id) {
|
||||
var self = this;
|
||||
var dialog = false;
|
||||
|
||||
if (follower_id) {
|
||||
dialog = true;
|
||||
}
|
||||
else {
|
||||
this.$('.o_subtypes_list ul').empty();
|
||||
if (! this.message_is_follower) {
|
||||
this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', true);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', false);
|
||||
}
|
||||
}
|
||||
if (this.follower_id) {
|
||||
return ajax.jsonRpc('/mail/read_subscription_data', 'call', {
|
||||
res_model: this.view.model,
|
||||
res_id: this.view.datarecord.id,
|
||||
follower_id: follower_id,
|
||||
}).then(function (data) {
|
||||
self.display_subtypes(data, dialog);
|
||||
});
|
||||
} else {
|
||||
return $.Deferred().resolve();
|
||||
}
|
||||
},
|
||||
|
||||
/** Display subtypes: {'name': default, followed} */
|
||||
display_subtypes:function (data, dialog) {
|
||||
var self = this;
|
||||
var old_parent_model;
|
||||
var $list;
|
||||
if (dialog) {
|
||||
$list = this.dialog.$el;
|
||||
} else {
|
||||
$list = this.$('.o_subtypes_list ul');
|
||||
}
|
||||
$list.empty();
|
||||
|
||||
this.data_subtype = data;
|
||||
this.records_length = $.map(data, function(value, index) { return index; }).length;
|
||||
|
||||
if (this.records_length > 1) {
|
||||
self.display_followers();
|
||||
}
|
||||
|
||||
_.each(data, function (record) {
|
||||
if (old_parent_model !== record.parent_model && old_parent_model !== undefined) {
|
||||
$list.append($('<li>').addClass('divider'));
|
||||
}
|
||||
old_parent_model = record.parent_model;
|
||||
record.followed = record.followed || undefined;
|
||||
$(qweb.render('mail.Followers.subtype', {'record': record,
|
||||
'dialog': dialog}))
|
||||
.appendTo($list);
|
||||
});
|
||||
},
|
||||
|
||||
do_follow: function () {
|
||||
var context = new data.CompoundContext(this.build_context(), {});
|
||||
this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', false);
|
||||
this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id],
|
||||
[session.uid],
|
||||
undefined,
|
||||
context])
|
||||
.then(this.proxy('read_value'));
|
||||
|
||||
_.each(this.$('.o_subtypes_list input'), function (record) {
|
||||
$(record).attr('checked', 'checked');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove users, partners, or channels from the followers
|
||||
* @param {Array} [ids.user_ids] the user ids
|
||||
* @param {Array} [ids.partner_ids] the partner ids
|
||||
* @param {Array} [ids.channel_ids] the channel ids
|
||||
*/
|
||||
do_unfollow: function (ids) {
|
||||
if (confirm(_t("Warning! \n If you remove a follower, he won't be notified of any email or discussion on this document. Do you really want to remove this follower ?"))) {
|
||||
_(this.$('.o_subtype_checkbox')).each(function (record) {
|
||||
$(record).attr('checked',false);
|
||||
});
|
||||
|
||||
this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', true);
|
||||
var context = new data.CompoundContext(this.build_context(), {});
|
||||
|
||||
if (ids.partner_ids || ids.channel_ids) {
|
||||
return this.ds_model.call(
|
||||
'message_unsubscribe', [
|
||||
[this.view.datarecord.id],
|
||||
ids.partner_ids,
|
||||
ids.channel_ids,
|
||||
context]
|
||||
).then(this.proxy('read_value'));
|
||||
} else {
|
||||
return this.ds_model.call(
|
||||
'message_unsubscribe_users', [
|
||||
[this.view.datarecord.id],
|
||||
ids.user_ids,
|
||||
context]
|
||||
).then(this.proxy('read_value'));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
do_update_subscription: function (event, follower_id, is_channel) {
|
||||
var self = this;
|
||||
var kwargs = {};
|
||||
var ids = {};
|
||||
var action_subscribe;
|
||||
var subtypes;
|
||||
this.data_subtype = {};
|
||||
|
||||
if (follower_id !== undefined) {
|
||||
// Subtypes edited from the modal
|
||||
action_subscribe = 'message_subscribe';
|
||||
subtypes = this.dialog.$('input[type="checkbox"]');
|
||||
if (is_channel) {
|
||||
ids.channel_ids = [follower_id];
|
||||
} else {
|
||||
ids.partner_ids = [follower_id];
|
||||
}
|
||||
} else {
|
||||
action_subscribe = 'message_subscribe_users';
|
||||
subtypes = this.$('.o_followers_actions input[type="checkbox"]');
|
||||
ids.user_ids = [session.uid];
|
||||
}
|
||||
kwargs = _.extend(kwargs, ids);
|
||||
|
||||
// Get the subtype ids
|
||||
var checklist = [];
|
||||
_(subtypes).each(function (record) {
|
||||
if ($(record).is(':checked')) {
|
||||
checklist.push(parseInt($(record).data('id')));
|
||||
}
|
||||
});
|
||||
kwargs.subtype_ids = checklist;
|
||||
|
||||
// If no more subtype followed, unsubscribe the follower
|
||||
if (!checklist.length) {
|
||||
if (!this.do_unfollow(ids)) {
|
||||
$(event.target).attr("checked", "checked");
|
||||
} else {
|
||||
self.$('.o_subtypes_list ul').empty();
|
||||
}
|
||||
} else {
|
||||
kwargs.context = new data.CompoundContext(this.build_context(), {});
|
||||
return this.ds_model._model.call(action_subscribe, [[this.view.datarecord.id]], kwargs).then(this.proxy('read_value'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Chat Composer for the Chatter
|
||||
//
|
||||
// Extends the basic Composer Widget to add 'suggested partner' layer (open
|
||||
// popup when suggested partner is selected without email, or other
|
||||
// informations), and the button to open the full composer wizard.
|
||||
// -----------------------------------------------------------------------------
|
||||
var ChatterComposer = ChatComposer.extend({
|
||||
template: 'mail.chatter.ChatComposer',
|
||||
|
||||
init: function (parent, dataset, options) {
|
||||
this._super(parent, options);
|
||||
this.thread_dataset = dataset;
|
||||
this.suggested_partners = [];
|
||||
this.options = _.defaults(this.options, {
|
||||
display_mode: 'textarea',
|
||||
record_name: false,
|
||||
is_log: false,
|
||||
internal_subtypes: [],
|
||||
});
|
||||
this.events = _.extend(this.events, {
|
||||
'click .o_composer_button_full_composer': 'on_open_full_composer',
|
||||
});
|
||||
},
|
||||
|
||||
willStart: function () {
|
||||
if (this.options.is_log) {
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
return $.when(this._super.apply(this, arguments), this.message_get_suggested_recipients());
|
||||
},
|
||||
|
||||
start: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
autosize(self.$input);
|
||||
});
|
||||
},
|
||||
|
||||
preprocess_message: function () {
|
||||
var self = this;
|
||||
var def = $.Deferred();
|
||||
this._super().then(function (message) {
|
||||
message = _.extend(message, {
|
||||
subtype_id: false,
|
||||
subtype: 'mail.mt_comment',
|
||||
message_type: 'comment',
|
||||
content_subtype: 'html',
|
||||
});
|
||||
|
||||
// Subtype
|
||||
if (self.options.is_log) {
|
||||
var subtype_id = parseInt(self.$('.o_chatter_composer_subtype_select').val());
|
||||
if (_.indexOf(_.pluck(self.options.internal_subtypes, 'id'), subtype_id) === -1) {
|
||||
message.subtype = 'mail.mt_note';
|
||||
} else {
|
||||
message.subtype_id = subtype_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Partner_ids
|
||||
if (!self.options.is_log) {
|
||||
var checked_suggested_partners = self.get_checked_suggested_partners();
|
||||
self.check_suggested_partners(checked_suggested_partners).done(function (partner_ids) {
|
||||
message.partner_ids = (message.partner_ids || []).concat(partner_ids);
|
||||
// update context
|
||||
message.context = _.defaults(self.context, {
|
||||
mail_post_autofollow: true,
|
||||
mail_post_autofollow_partner_ids: partner_ids,
|
||||
});
|
||||
def.resolve(message);
|
||||
});
|
||||
} else {
|
||||
def.resolve(message);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return def;
|
||||
},
|
||||
|
||||
/**
|
||||
* Override keydown event handler to go to next line on ENTER and send message on CTRL+ENTER
|
||||
*/
|
||||
on_keydown: function (event) {
|
||||
if (!event.ctrlKey && (event.keyCode === $.ui.keyCode.ENTER) && !this.get('mention_partners').length) {
|
||||
return;
|
||||
}
|
||||
this._super(event);
|
||||
},
|
||||
|
||||
message_get_suggested_recipients: function () {
|
||||
var self = this;
|
||||
var email_addresses = _.pluck(this.suggested_partners, 'email_address');
|
||||
return this.thread_dataset
|
||||
.call('message_get_suggested_recipients', [[this.context.default_res_id], this.context])
|
||||
.done(function (suggested_recipients) {
|
||||
var thread_recipients = suggested_recipients[self.context.default_res_id];
|
||||
_.each(thread_recipients, function (recipient) {
|
||||
var parsed_email = parse_email(recipient[1]);
|
||||
if (_.indexOf(email_addresses, parsed_email[1]) === -1) {
|
||||
self.suggested_partners.push({
|
||||
checked: true,
|
||||
partner_id: recipient[0],
|
||||
full_name: recipient[1],
|
||||
name: parsed_email[0],
|
||||
email_address: parsed_email[1],
|
||||
reason: recipient[2],
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the list of selected suggested partners
|
||||
* @returns Array() : list of 'recipient' selected partners (may not be created in db)
|
||||
**/
|
||||
get_checked_suggested_partners: function () {
|
||||
var self = this;
|
||||
var checked_partners = [];
|
||||
this.$('.o_composer_suggested_partners input:checked').each(function() {
|
||||
var full_name = $(this).data('fullname');
|
||||
checked_partners = checked_partners.concat(_.filter(self.suggested_partners, function(item) {
|
||||
return full_name === item.full_name;
|
||||
}));
|
||||
});
|
||||
return checked_partners;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the additionnal partners (not necessary registered partners), and open a popup form view
|
||||
* for the ones who informations is missing.
|
||||
* @param Array : list of 'recipient' partners to complete informations or validate
|
||||
* @returns Deferred resolved with the list of checked suggested partners (real partner)
|
||||
**/
|
||||
check_suggested_partners: function (checked_suggested_partners) {
|
||||
var self = this;
|
||||
var check_done = $.Deferred();
|
||||
|
||||
var recipients = _.filter(checked_suggested_partners, function (recipient) { return recipient.checked; });
|
||||
var recipients_to_find = _.filter(recipients, function (recipient) { return (! recipient.partner_id); });
|
||||
var names_to_find = _.pluck(recipients_to_find, 'full_name');
|
||||
var recipients_to_check = _.filter(recipients, function (recipient) { return (recipient.partner_id && ! recipient.email_address); });
|
||||
var recipient_ids = _.pluck(_.filter(recipients, function (recipient) { return recipient.partner_id && recipient.email_address; }), 'partner_id');
|
||||
|
||||
var names_to_remove = [];
|
||||
var recipient_ids_to_remove = [];
|
||||
|
||||
// have unknown names -> call message_get_partner_info_from_emails to try to find partner_id
|
||||
var find_done = $.Deferred();
|
||||
if (names_to_find.length > 0) {
|
||||
find_done = self.thread_dataset.call('message_partner_info_from_emails', [[this.context.default_res_id], names_to_find]);
|
||||
} else {
|
||||
find_done.resolve([]);
|
||||
}
|
||||
|
||||
// for unknown names + incomplete partners -> open popup - cancel = remove from recipients
|
||||
$.when(find_done).pipe(function (result) {
|
||||
var emails_deferred = [];
|
||||
var recipient_popups = result.concat(recipients_to_check);
|
||||
|
||||
_.each(recipient_popups, function (partner_info) {
|
||||
var deferred = $.Deferred();
|
||||
emails_deferred.push(deferred);
|
||||
|
||||
var partner_name = partner_info.full_name;
|
||||
var partner_id = partner_info.partner_id;
|
||||
var parsed_email = parse_email(partner_name);
|
||||
|
||||
var dialog = new form_common.FormViewDialog(self, {
|
||||
res_model: 'res.partner',
|
||||
res_id: partner_id,
|
||||
context: {
|
||||
force_email: true,
|
||||
ref: "compound_context",
|
||||
default_name: parsed_email[0],
|
||||
default_email: parsed_email[1],
|
||||
},
|
||||
title: _t("Please complete partner's informations"),
|
||||
}).open();
|
||||
dialog.on('closed', self, function () {
|
||||
deferred.resolve();
|
||||
});
|
||||
dialog.view_form.on('on_button_cancel', self, function () {
|
||||
names_to_remove.push(partner_name);
|
||||
if (partner_id) {
|
||||
recipient_ids_to_remove.push(partner_id);
|
||||
}
|
||||
});
|
||||
});
|
||||
$.when.apply($, emails_deferred).then(function () {
|
||||
var new_names_to_find = _.difference(names_to_find, names_to_remove);
|
||||
find_done = $.Deferred();
|
||||
if (new_names_to_find.length > 0) {
|
||||
find_done = self.thread_dataset.call('message_partner_info_from_emails', [[self.context.default_res_id], new_names_to_find, true]);
|
||||
} else {
|
||||
find_done.resolve([]);
|
||||
}
|
||||
$.when(find_done).pipe(function (result) {
|
||||
var recipient_popups = result.concat(recipients_to_check);
|
||||
_.each(recipient_popups, function (partner_info) {
|
||||
if (partner_info.partner_id && _.indexOf(partner_info.partner_id, recipient_ids_to_remove) === -1) {
|
||||
recipient_ids.push(partner_info.partner_id);
|
||||
}
|
||||
});
|
||||
}).pipe(function () {
|
||||
check_done.resolve(recipient_ids);
|
||||
});
|
||||
});
|
||||
});
|
||||
return check_done;
|
||||
},
|
||||
|
||||
on_open_full_composer: function() {
|
||||
if (!this.do_check_attachment_upload()){
|
||||
return false;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var recipient_done = $.Deferred();
|
||||
if (this.options.is_log) {
|
||||
recipient_done.resolve([]);
|
||||
} else {
|
||||
var checked_suggested_partners = this.get_checked_suggested_partners();
|
||||
recipient_done = this.check_suggested_partners(checked_suggested_partners);
|
||||
}
|
||||
recipient_done.then(function (partner_ids) {
|
||||
var context = {
|
||||
default_parent_id: self.id,
|
||||
default_body: get_text2html(self.$input.val()),
|
||||
default_attachment_ids: _.pluck(self.get('attachment_ids'), 'id'),
|
||||
default_partner_ids: partner_ids,
|
||||
default_is_log: self.options.is_log,
|
||||
mail_post_autofollow: true,
|
||||
mail_post_autofollow_partner_ids: partner_ids,
|
||||
};
|
||||
|
||||
if (self.context.default_model && self.context.default_res_id) {
|
||||
context.default_model = self.context.default_model;
|
||||
context.default_res_id = self.context.default_res_id;
|
||||
}
|
||||
|
||||
self.do_action({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'mail.compose.message',
|
||||
view_mode: 'form',
|
||||
view_type: 'form',
|
||||
views: [[false, 'form']],
|
||||
target: 'new',
|
||||
context: context,
|
||||
}, {
|
||||
on_close: function() {
|
||||
self.trigger('need_refresh');
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Document Chatter ('mail_thread' widget)
|
||||
//
|
||||
// Since it is displayed on a form view, it extends 'AbstractField' widget.
|
||||
// -----------------------------------------------------------------------------
|
||||
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",
|
||||
},
|
||||
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.messages = [];
|
||||
this.model = this.view.dataset.model;
|
||||
this.res_id = undefined;
|
||||
this.context = this.options.context || {};
|
||||
},
|
||||
|
||||
start: function () {
|
||||
var self = this;
|
||||
|
||||
// Move the follower's widget (if any) inside the chatter
|
||||
var followers = this.field_manager.fields.message_follower_ids;
|
||||
if (followers) {
|
||||
this.$('.o_chatter_topbar').append(followers.$el);
|
||||
}
|
||||
|
||||
this.thread = new ChatThread(this, {
|
||||
display_order: ChatThread.ORDER.DESC,
|
||||
display_document_link: false,
|
||||
});
|
||||
this.thread.on('load_more_messages', this, this.load_more_messages);
|
||||
this.thread.on('toggle_star_status', this, function (message_id) {
|
||||
chat_manager.toggle_star_status(message_id);
|
||||
});
|
||||
|
||||
var def1 = this._super.apply(this, arguments);
|
||||
var def2 = this.thread.appendTo(this.$el);
|
||||
|
||||
return $.when(def1, def2).then(function () {
|
||||
chat_manager.bus.on('new_message', self, self.on_new_message);
|
||||
chat_manager.bus.on('update_message', self, self.on_update_message);
|
||||
});
|
||||
},
|
||||
|
||||
fetch_and_render_thread: function (ids, options) {
|
||||
var self = this;
|
||||
return chat_manager.fetch_messages(ids, options).then(function (raw_messages) {
|
||||
self.thread.render(raw_messages, {display_load_more: raw_messages.length < ids.length});
|
||||
});
|
||||
},
|
||||
|
||||
on_post_message: function (message) {
|
||||
var self = this;
|
||||
chat_manager
|
||||
.post_message_in_document(this.model, this.res_id, message)
|
||||
.then(function () {
|
||||
self.close_composer();
|
||||
})
|
||||
.fail(function () {
|
||||
// todo: display notification
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* When a message is correctly posted, fetch its data to render it
|
||||
* @param {Number} message_id : the identifier of the new posted message
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
on_new_message: function (message) {
|
||||
if (message.model === this.model && message.res_id === this.res_id) {
|
||||
this.msg_ids.unshift(message.id);
|
||||
this.fetch_and_render_thread(this.msg_ids);
|
||||
}
|
||||
},
|
||||
|
||||
on_update_message: function (message) {
|
||||
if (message.model === this.model && message.res_id === this.res_id) {
|
||||
this.fetch_and_render_thread(this.msg_ids);
|
||||
}
|
||||
},
|
||||
|
||||
on_click_redirect: function (event) {
|
||||
event.preventDefault();
|
||||
this.do_action({
|
||||
type:'ir.actions.act_window',
|
||||
view_type: 'form',
|
||||
view_mode: 'form',
|
||||
res_model: $(event.target).data('oe-model'),
|
||||
views: [[false, 'form']],
|
||||
res_id: $(event.target).data('oe-id'),
|
||||
});
|
||||
},
|
||||
|
||||
load_more_messages: function () {
|
||||
this.fetch_and_render_thread(this.msg_ids, {force_fetch: true});
|
||||
},
|
||||
|
||||
/**
|
||||
* The value of the field has change (modification or switch to next document). Form view is
|
||||
* not re-rendered, simply updated. A re-fetch is needed.
|
||||
* @override
|
||||
*/
|
||||
render_value: function () {
|
||||
// update context
|
||||
this.context = _.extend({
|
||||
default_res_id: this.view.datarecord.id || false,
|
||||
default_model: this.view.model || false,
|
||||
}, this.options.context || {});
|
||||
this.thread_dataset = this.view.dataset;
|
||||
this.res_id = this.view.datarecord.id;
|
||||
this.record_name = this.view.datarecord.display_name;
|
||||
this.msg_ids = this.get_value() || [];
|
||||
|
||||
// destroy current composer, if any
|
||||
if (this.composer) {
|
||||
this.composer.destroy();
|
||||
}
|
||||
|
||||
// fetch and render messages of current document
|
||||
return this.fetch_and_render_thread(this.msg_ids);
|
||||
},
|
||||
// composer toggle
|
||||
on_open_composer_new_message: function () {
|
||||
this.open_composer();
|
||||
},
|
||||
on_open_composer_log_note: function () {
|
||||
this.open_composer({is_log: true});
|
||||
},
|
||||
open_composer: function (options) {
|
||||
var self = this;
|
||||
var old_composer = this.composer;
|
||||
// create the new composer
|
||||
this.composer = new ChatterComposer(this, this.thread_dataset, {
|
||||
internal_subtypes: this.options.internal_subtypes,
|
||||
is_log: options && options.is_log,
|
||||
context: this.context,
|
||||
record_name: this.record_name,
|
||||
});
|
||||
this.composer.insertBefore(this.$('.o_mail_thread')).then(function () {
|
||||
// destroy existing composer
|
||||
if (old_composer) {
|
||||
old_composer.destroy();
|
||||
}
|
||||
self.composer.on('post_message', self, self.on_post_message);
|
||||
});
|
||||
|
||||
},
|
||||
close_composer: function () {
|
||||
if (this.composer.is_empty()) {
|
||||
this.composer.do_hide();
|
||||
}
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
chat_manager.remove_chatter_messages(this.model);
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Utils
|
||||
// -----------------------------------------------------------------------------
|
||||
/**
|
||||
* Parses text to find email: Tagada <address@mail.fr> -> [Tagada, address@mail.fr] or False
|
||||
*/
|
||||
var parse_email = function (text) {
|
||||
var result = text.match(/(.*)<(.*@.*)>/);
|
||||
if (result) {
|
||||
return [_.str.trim(result[1]), _.str.trim(result[2])];
|
||||
}
|
||||
result = text.match(/(.*@.*)/);
|
||||
if (result) {
|
||||
return [_.str.trim(result[1]), _.str.trim(result[1])];
|
||||
}
|
||||
return [text, false];
|
||||
};
|
||||
/**
|
||||
* Replaces textarea text into html text (add <p>, <a>)
|
||||
* TDE note : should be done server-side, in Python -> use mail.compose.message ?
|
||||
*/
|
||||
var get_text2html = function (text) {
|
||||
return text
|
||||
.replace(/((?:https?|ftp):\/\/[\S]+)/g,'<a href="$1">$1</a> ')
|
||||
.replace(/[\n\r]/g,'<br/>');
|
||||
};
|
||||
|
||||
core.form_widget_registry.add('mail_followers', Followers);
|
||||
core.form_widget_registry.add('mail_thread', Chatter);
|
||||
|
||||
return Chatter;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,486 @@
|
||||
odoo.define('mail.ChatComposer', function (require) {
|
||||
"use strict";
|
||||
|
||||
var chat_manager = require('mail.chat_manager');
|
||||
|
||||
var core = require('web.core');
|
||||
var data = require('web.data');
|
||||
var Model = require('web.Model');
|
||||
var session = require('web.session');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
var Composer = Widget.extend({
|
||||
template: "mail.ChatComposer",
|
||||
|
||||
events: {
|
||||
"keydown .o_composer_input": "on_keydown",
|
||||
"keyup .o_composer_input": "on_keyup",
|
||||
"change input.o_form_input_file": "on_attachment_change",
|
||||
"click .o_composer_button_send": "send_message",
|
||||
"click .o_composer_button_add_attachment": "on_click_add_attachment",
|
||||
"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) {
|
||||
this._super.apply(this, arguments);
|
||||
this.options = _.defaults(options || {}, {
|
||||
context: {},
|
||||
mention_delimiter: '@',
|
||||
mention_min_length: 2,
|
||||
mention_typing_speed: 400,
|
||||
mention_fetch_limit: 8,
|
||||
});
|
||||
this.context = this.options.context;
|
||||
|
||||
// Attachments
|
||||
this.AttachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context);
|
||||
this.fileupload_id = _.uniqueId('o_chat_fileupload');
|
||||
this.set('attachment_ids', []);
|
||||
|
||||
// Mention
|
||||
this.PartnerModel = new Model('res.partner');
|
||||
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 () {
|
||||
var self = this;
|
||||
|
||||
this.$attachment_button = this.$(".o_composer_button_add_attachment");
|
||||
this.$attachments_list = this.$('.o_composer_attachments_list');
|
||||
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.focus();
|
||||
|
||||
// Attachments
|
||||
$(window).on(this.fileupload_id, this.on_attachment_loaded);
|
||||
this.on("change:attachment_ids", this, this.render_attachments);
|
||||
|
||||
// Emoji
|
||||
this.$('.o_composer_button_emoji').popover({
|
||||
placement: 'top',
|
||||
content: function() {
|
||||
if (!self.$emojis) { // lazy rendering
|
||||
self.$emojis = $(QWeb.render('mail.ChatComposer.emojis', {
|
||||
emojis: chat_manager.get_emojis(),
|
||||
}));
|
||||
self.$emojis.filter('.o_mail_emoji').on('click', self, self.on_click_emoji_img);
|
||||
}
|
||||
return self.$emojis;
|
||||
},
|
||||
html: true,
|
||||
container: '.o_composer_emoji',
|
||||
trigger: 'focus',
|
||||
});
|
||||
|
||||
// Mention
|
||||
this.on('change:mention_partners', this, this.render_mention_partners);
|
||||
this.on('change:mention_selected_partners', this, this.render_mention_selected_partners);
|
||||
return this._super();
|
||||
},
|
||||
|
||||
preprocess_message: function () {
|
||||
// Return a deferred as this function is extended with asynchronous
|
||||
// behavior for the chatter composer
|
||||
return $.when({
|
||||
content: this.mention_preprocess_message(this.$input.val()),
|
||||
attachment_ids: _.pluck(this.get('attachment_ids'), 'id'),
|
||||
partner_ids: _.pluck(this.get('mention_selected_partners'), 'id'),
|
||||
});
|
||||
},
|
||||
|
||||
send_message: function () {
|
||||
if (this.is_empty() || !this.do_check_attachment_upload()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.preprocess_message().then(function (message) {
|
||||
self.trigger('post_message', message);
|
||||
|
||||
// Empty input, selected partners and attachments
|
||||
self.$input.val('');
|
||||
self.set('mention_selected_partners', []);
|
||||
self.set('attachment_ids', []);
|
||||
|
||||
self.$input.focus();
|
||||
});
|
||||
},
|
||||
|
||||
// Events
|
||||
on_click_add_attachment: function () {
|
||||
this.$('input.o_form_input_file').click();
|
||||
this.$input.focus();
|
||||
},
|
||||
|
||||
on_click_emoji_img: function(event) {
|
||||
this.$input.val(this.$input.val() + " " + $(event.currentTarget).data('emoji') + " ");
|
||||
this.$input.focus();
|
||||
},
|
||||
|
||||
on_keydown: function (event) {
|
||||
switch(event.which) {
|
||||
// UP, DOWN: prevent moving cursor if navigation in mention propositions
|
||||
case $.ui.keyCode.UP:
|
||||
case $.ui.keyCode.DOWN:
|
||||
if (this.get('mention_partners').length) {
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
// BACKSPACE, DELETE: check if need to remove a mention
|
||||
case $.ui.keyCode.BACKSPACE:
|
||||
case $.ui.keyCode.DELETE:
|
||||
this.mention_check_remove();
|
||||
break;
|
||||
// ENTER: submit the message only if the dropdown mention proposition is not displayed
|
||||
case $.ui.keyCode.ENTER:
|
||||
event.preventDefault();
|
||||
if (!this.get('mention_partners').length) {
|
||||
this.send_message();
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
on_keyup: function (event) {
|
||||
switch(event.which) {
|
||||
// ESCAPED KEYS: do nothing
|
||||
case $.ui.keyCode.END:
|
||||
case $.ui.keyCode.PAGE_UP:
|
||||
case $.ui.keyCode.PAGE_DOWN:
|
||||
break;
|
||||
// ESCAPE: close mention propositions
|
||||
case $.ui.keyCode.ESCAPE:
|
||||
this.set('mention_partners', []);
|
||||
break;
|
||||
// ENTER, UP, DOWN: check if navigation in mention propositions
|
||||
case $.ui.keyCode.ENTER:
|
||||
case $.ui.keyCode.UP:
|
||||
case $.ui.keyCode.DOWN:
|
||||
this.mention_proposition_navigation(event.which);
|
||||
break;
|
||||
// Otherwise, check if a mention is typed
|
||||
default:
|
||||
this.mention_word = this.mention_detect_delimiter();
|
||||
if (this.mention_word) {
|
||||
this.mention_word_changed();
|
||||
} else {
|
||||
this.set('mention_partners', []); // close the dropdown
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Attachments
|
||||
on_attachment_change: function(event) {
|
||||
var $target = $(event.target);
|
||||
if ($target.val() !== '') {
|
||||
var filename = $target.val().replace(/.*[\\\/]/,'');
|
||||
// if the files exits for this answer, delete the file before upload
|
||||
var attachments = [];
|
||||
for (var i in this.get('attachment_ids')) {
|
||||
if ((this.get('attachment_ids')[i].filename || this.get('attachment_ids')[i].name) === filename) {
|
||||
if (this.get('attachment_ids')[i].upload) {
|
||||
return false;
|
||||
}
|
||||
this.AttachmentDataSet.unlink([this.get('attachment_ids')[i].id]);
|
||||
} else {
|
||||
attachments.push(this.get('attachment_ids')[i]);
|
||||
}
|
||||
}
|
||||
// submit filename
|
||||
this.$('form.o_form_binary_form').submit();
|
||||
this.$attachment_button.prop('disabled', true);
|
||||
|
||||
attachments.push({
|
||||
'id': 0,
|
||||
'name': filename,
|
||||
'filename': filename,
|
||||
'url': '',
|
||||
'upload': true,
|
||||
'mimetype': '',
|
||||
});
|
||||
this.set('attachment_ids', attachments);
|
||||
}
|
||||
},
|
||||
on_attachment_loaded: function(event, result) {
|
||||
var attachment_ids = [];
|
||||
if (result.error || !result.id ) {
|
||||
this.do_warn(result.error);
|
||||
attachment_ids = _.filter(this.get('attachment_ids'), function (val) { return !val.upload; });
|
||||
} else {
|
||||
_.each(this.get('attachment_ids'), function(a) {
|
||||
if (a.filename === result.filename && a.upload) {
|
||||
attachment_ids.push({
|
||||
'id': result.id,
|
||||
'name': result.name || result.filename,
|
||||
'filename': result.filename,
|
||||
'mimetype': result.mimetype,
|
||||
'url': session.url('/web/content', {'id': result.id, download: true}),
|
||||
});
|
||||
} else {
|
||||
attachment_ids.push(a);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.set('attachment_ids', attachment_ids);
|
||||
this.$attachment_button.prop('disabled', false);
|
||||
},
|
||||
on_attachment_delete: function(event){
|
||||
event.stopPropagation();
|
||||
var self = this;
|
||||
var attachment_id = $(event.target).data("id");
|
||||
if (attachment_id) {
|
||||
var attachments = [];
|
||||
_.each(this.get('attachment_ids'), function(attachment){
|
||||
if (attachment_id !== attachment.id) {
|
||||
attachments.push(attachment);
|
||||
} else {
|
||||
self.AttachmentDataSet.unlink([attachment_id]);
|
||||
}
|
||||
});
|
||||
this.set('attachment_ids', attachments);
|
||||
}
|
||||
},
|
||||
do_check_attachment_upload: function () {
|
||||
if (_.find(this.get('attachment_ids'), function (file) { return file.upload; })) {
|
||||
this.do_warn(_t("Uploading error"), _t("Please, wait while the file is uploading."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
render_attachments: function() {
|
||||
this.$attachments_list.html(QWeb.render('mail.ChatComposer.Attachments', {
|
||||
attachments: this.get('attachment_ids'),
|
||||
}));
|
||||
},
|
||||
|
||||
// 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(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.next('.o_mention_proposition:not(.active)');
|
||||
} else {
|
||||
$to = $active.prev('.o_mention_proposition:not(.active)');
|
||||
}
|
||||
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_str) {
|
||||
var self = this;
|
||||
this.PartnerModel
|
||||
.query(['id', 'name', 'email'])
|
||||
.filter([['id', 'not in', _.pluck(this.get('mention_selected_partners'), 'id')],
|
||||
'|', ['name', 'ilike', search_str], ['email', 'ilike', search_str]])
|
||||
.limit(this.options.mention_fetch_limit)
|
||||
.all().then(function (partners) {
|
||||
self.set('mention_partners', partners);
|
||||
});
|
||||
},
|
||||
|
||||
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<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_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 (this.get('mention_partners').length) {
|
||||
this.$mention_dropdown.html(QWeb.render('mail.ChatComposer.MentionMenu', {
|
||||
partners: this.get('mention_partners'),
|
||||
}));
|
||||
this.$mention_dropdown.addClass('open');
|
||||
} else {
|
||||
this.$mention_dropdown.removeClass('open');
|
||||
this.$mention_dropdown.empty();
|
||||
}
|
||||
},
|
||||
|
||||
render_mention_selected_partners: function () {
|
||||
this.$mention_partner_tags.html(QWeb.render('mail.ChatComposer.MentionTags', {
|
||||
partners: this.get('mention_selected_partners'),
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
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 () {
|
||||
return !this.$input.val().trim() && !this.$('.o_attachments').children().length;
|
||||
},
|
||||
});
|
||||
|
||||
return Composer;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
odoo.define('mail.ChatThread', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
var ORDER = {
|
||||
ASC: 1,
|
||||
DESC: -1,
|
||||
};
|
||||
|
||||
var Thread = Widget.extend({
|
||||
className: 'o_mail_thread',
|
||||
|
||||
events: {
|
||||
"click .o_mail_redirect": "on_click_redirect",
|
||||
"click .o_thread_show_more": "on_click_show_more",
|
||||
"click .o_thread_message_needaction": function (event) {
|
||||
var message_id = $(event.currentTarget).data('message-id');
|
||||
this.trigger("mark_as_read", message_id);
|
||||
},
|
||||
"click .o_thread_message_star": function (event) {
|
||||
var message_id = $(event.currentTarget).data('message-id');
|
||||
this.trigger("toggle_star_status", message_id);
|
||||
},
|
||||
},
|
||||
|
||||
init: function (parent, options) {
|
||||
this._super.apply(this, arguments);
|
||||
this.options = _.defaults(options || {}, {
|
||||
display_order: ORDER.ASC,
|
||||
display_needactions: true,
|
||||
default_username: _t('Anonymous'),
|
||||
display_document_link: true,
|
||||
display_avatar: true,
|
||||
});
|
||||
},
|
||||
|
||||
render: function (messages, options) {
|
||||
var msgs = _.map(messages, this._preprocess_message.bind(this));
|
||||
if (this.options.display_order === ORDER.DESC) {
|
||||
msgs.reverse();
|
||||
}
|
||||
this.$el.html(QWeb.render('mail.ChatThread', {
|
||||
messages: msgs,
|
||||
options: _.extend({}, this.options, options),
|
||||
ORDER: ORDER,
|
||||
}));
|
||||
},
|
||||
|
||||
on_click_redirect: function (event) {
|
||||
event.preventDefault();
|
||||
var res_id = $(event.target).data('oe-id');
|
||||
var res_model = $(event.target).data('oe-model');
|
||||
this.trigger('redirect', res_model, res_id);
|
||||
},
|
||||
|
||||
on_click_show_more: function () {
|
||||
this.trigger('load_more_messages');
|
||||
},
|
||||
|
||||
_preprocess_message: function (message) {
|
||||
var msg = _.extend({}, message);
|
||||
|
||||
// Compute displayed author
|
||||
if (!msg.author_id || !msg.author_id[1]) {
|
||||
msg.displayed_author = msg.email_from || this.options.default_username;
|
||||
}
|
||||
|
||||
// Compute the avatar_url
|
||||
if (msg.author_id && msg.author_id[0]) {
|
||||
msg.avatar_src = "/web/image/res.partner/" + msg.author_id[0] + "/image_small";
|
||||
} else if (msg.msg_type === 'email') {
|
||||
msg.avatar_src = "/mail/static/src/img/email_icon.png";
|
||||
} else {
|
||||
msg.avatar_src = "/mail/static/src/img/smiley/avatar.jpg";
|
||||
}
|
||||
|
||||
// Compute url of attachments
|
||||
_.each(msg.attachment_ids, function(a) {
|
||||
a.url = '/web/content/' + a.id + '?download=true';
|
||||
});
|
||||
|
||||
return msg;
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a message and re-renders the thread
|
||||
* @param {int} [message_id] the id of the removed message
|
||||
* @param {array} [messages] the list of messages to display, without the removed one
|
||||
*/
|
||||
remove_message_and_render: function (message_id, messages) {
|
||||
var self = this;
|
||||
this.$('.o_thread_message[data-message-id=' + message_id + ']').fadeOut({
|
||||
done: function () { self.render(messages); }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Scrolls the thread to a given message or offset if any, to bottom otherwise
|
||||
* @param {int} [target.id] optional: the id of the message to scroll to
|
||||
* @param {int} [target.offset] optional: the number of pixels to scroll
|
||||
*/
|
||||
scroll_to: function (target) {
|
||||
target = target || {};
|
||||
if (target.id !== undefined) {
|
||||
this.$el.scrollTo(this.$('.o_thread_message[data-message-id=' + target.id + ']'));
|
||||
} else if (target.offset !== undefined) {
|
||||
this.$el.scrollTop(target.offset);
|
||||
} else {
|
||||
this.$el.scrollTop(this.el.scrollHeight);
|
||||
}
|
||||
},
|
||||
get_scrolltop: function () {
|
||||
return this.$el.scrollTop();
|
||||
},
|
||||
});
|
||||
|
||||
Thread.ORDER = ORDER;
|
||||
|
||||
return Thread;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
.o_chatter {
|
||||
width: 100%;
|
||||
|
||||
.o_chat_composer {
|
||||
margin-top: 10px;
|
||||
|
||||
.o_chatter_composer_info {
|
||||
display: inline-block;
|
||||
margin-bottom: 5px;
|
||||
color: @odoo-main-color-muted;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.o_chatter_composer_subtypes {
|
||||
> label {
|
||||
display: inline-block; // Required for old design compatibility
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.o_chatter_composer_subtype_select {
|
||||
display: inline-block;
|
||||
min-width: 150px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.o_composer {
|
||||
margin-top: 10px;
|
||||
|
||||
.o_composer_input {
|
||||
width: 100%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_mail_thread {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_followers {
|
||||
float: right;
|
||||
@media (max-width: @screen-xs-max) {
|
||||
display: none; // Hide the followers in mobile
|
||||
}
|
||||
|
||||
.o_followers_actions {
|
||||
position: relative;
|
||||
float: right;
|
||||
|
||||
.o_followers_follow_button {
|
||||
width: 120px;
|
||||
|
||||
.o_follow, .o_followers_unfollow, .o_followers_following {
|
||||
display: none;
|
||||
}
|
||||
// when following, show 'following'
|
||||
&.o_followers_following .o_followers_following {
|
||||
display: inline;
|
||||
}
|
||||
// when following and hovering, show 'unfollow'
|
||||
&.o_followers_following:hover .o_followers_following {
|
||||
display: none;
|
||||
}
|
||||
&.o_followers_following:hover .o_followers_unfollow {
|
||||
display: inline;
|
||||
}
|
||||
// when not following show 'follow'
|
||||
&.o_followers_notfollow .o_follow {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.o_subtype {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.o_followers_title_box {
|
||||
position: relative;
|
||||
float: right;
|
||||
margin-left: 5px;
|
||||
> button {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.o_followers_count {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.o_followers_list {
|
||||
font-size: small;
|
||||
|
||||
.o_partner {
|
||||
@partner_avatar_size: 24px;
|
||||
position: relative; // required for old design compatibility
|
||||
> a {
|
||||
max-width: 160px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
height: @partner_avatar_size;
|
||||
padding: 3px @partner_avatar_size + 8px;
|
||||
}
|
||||
> img {
|
||||
.o-position-absolute();
|
||||
width: @partner_avatar_size;
|
||||
}
|
||||
.o_remove_follower {
|
||||
.o-position-absolute(6px, 5px);
|
||||
cursor: pointer;
|
||||
}
|
||||
.o_edit_subtype {
|
||||
.o-position-absolute(6px, 18px);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Edit subtypes in dialog
|
||||
.modal .modal-body > li.o_subtype {
|
||||
list-style: none;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
.o_composer_mentioned_partners, .o_composer_attachments_list {
|
||||
margin-top: 5px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
// Emoji size in the popover
|
||||
.o_mail_emoji {
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
||||
> img {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Attachment Icons (common for chat thread and chat composer)
|
||||
// ------------------------------------------------------------------
|
||||
@-webkit-keyframes oe_mail_attach_loading_anim {
|
||||
0% { background: @odoo-brand-optional }
|
||||
50% { background: darken(@odoo-brand-optional, 10%) }
|
||||
100% { background: @odoo-brand-optional }
|
||||
}
|
||||
@-moz-keyframes oe_mail_attach_loading_anim {
|
||||
0% { background: @odoo-brand-optional }
|
||||
50% { background: darken(@odoo-brand-optional, 10%) }
|
||||
100% { background: @odoo-brand-optional }
|
||||
}
|
||||
@-o-keyframes oe_mail_attach_loading_anim {
|
||||
0% { background: @odoo-brand-optional }
|
||||
50% { background: darken(@odoo-brand-optional, 10%) }
|
||||
100% { background: @odoo-brand-optional }
|
||||
}
|
||||
@keyframes oe_mail_attach_loading_anim {
|
||||
0% { background: @odoo-brand-optional }
|
||||
50% { background: darken(@odoo-brand-optional, 10%) }
|
||||
100% { background: @odoo-brand-optional }
|
||||
}
|
||||
|
||||
@o-mail-attachment-image-size: 60px;
|
||||
.o_attachment {
|
||||
position: relative;
|
||||
width: 2*@o-mail-attachment-image-size;
|
||||
display: inline-block;
|
||||
height: @o-mail-attachment-image-size;
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.o_image {
|
||||
.o-position-absolute(@top: 0, @left: @o-mail-attachment-image-size/2);
|
||||
width: @o-mail-attachment-image-size;
|
||||
height: @o-mail-attachment-image-size;
|
||||
}
|
||||
|
||||
.o_attachment_name {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
margin-top: @o-mail-attachment-image-size;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: @odoo-main-text-color;
|
||||
}
|
||||
|
||||
.o_attachment_progress_bar {
|
||||
display: none;
|
||||
}
|
||||
.o_attachment_delete {
|
||||
.o-position-absolute(@top: 0, @right: 0);
|
||||
color: @odoo-main-text-color;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
.o-transition(opacity, 0.2s, linear);
|
||||
}
|
||||
&.o_attachment_uploading {
|
||||
.o_attachment_progress_bar {
|
||||
display: block;
|
||||
.o-position-absolute(@top: 18px, @left: 16px, @right: 16px);
|
||||
height: 17px;
|
||||
line-height: 13px;
|
||||
color: white;
|
||||
border-radius: @border-radius-base;
|
||||
border: solid 1px grey;
|
||||
box-shadow: 0px 1px 10px @odoo-shadow-color;
|
||||
.o-animation(@name: oe_mail_attach_loading_anim, @duration: 1s, @timing-function: linear, @iteration-count: infinite);
|
||||
}
|
||||
.o_attachment_delete {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .o_attachment_delete {
|
||||
opacity: 1;
|
||||
.o-transition(opacity, 0.2s, linear);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
@mail-thread-avatar-size: 50px;
|
||||
|
||||
.o_mail_thread {
|
||||
.o_thread_show_more {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.o_thread_date_separator {
|
||||
margin-bottom: 15px;
|
||||
border-bottom: solid @gray-lighter 2px;
|
||||
text-align: center;
|
||||
|
||||
.o_thread_date {
|
||||
position: relative;
|
||||
top: 10px;
|
||||
margin: 0 auto;
|
||||
padding: 0 10px;
|
||||
font-weight: bold;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
.o_thread_message {
|
||||
.o-flex-display();
|
||||
margin: 10px 0;
|
||||
|
||||
.o_thread_message_avatar {
|
||||
.o-flex(0, 0, @mail-thread-avatar-size);
|
||||
margin-right: 10px;
|
||||
@media (max-width: @screen-xs-max) {
|
||||
.o-flex(0, 0, @mail-thread-avatar-size/2);
|
||||
}
|
||||
|
||||
.o_thread_message_avatar_img {
|
||||
display: block;
|
||||
margin: 0 auto; // center the avatar
|
||||
max-width: @mail-thread-avatar-size;
|
||||
@media (max-width: @screen-xs-max) {
|
||||
max-width: @mail-thread-avatar-size/2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_thread_message_core {
|
||||
.o-flex(1, 1, auto);
|
||||
|
||||
.o_mail_subject {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
> p {
|
||||
margin: 0 0 9px; // Required by the old design to override a general rule on p's
|
||||
}
|
||||
|
||||
.o_thread_message_star, .o_thread_message_needaction {
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
&.fa-star {
|
||||
color: gold;
|
||||
}
|
||||
}
|
||||
|
||||
.o_mail_redirect {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
|
||||
<!--
|
||||
Chatter composer
|
||||
-->
|
||||
<t t-name="mail.chatter.ChatComposer" t-extend="mail.ChatComposer">
|
||||
<!-- Insert information before the composer -->
|
||||
<t t-jquery=".o_composer" t-operation="before">
|
||||
<!-- Internal note composer -->
|
||||
<t t-if="widget.options.is_log">
|
||||
<span class="o_chatter_composer_info">
|
||||
Log an internal note which will not be sent to followers, but which can be read
|
||||
by users accessing this document.
|
||||
</span>
|
||||
<!-- Internal subtypes -->
|
||||
<div t-att-class="'o_chatter_composer_subtypes ' +
|
||||
(widget.options.internal_subtypes.length <= 1 ? ' hide' : '')">
|
||||
<label>Activity</label>
|
||||
<select class="o_chatter_composer_subtype_select">
|
||||
<t t-foreach="widget.options.internal_subtypes" t-as="subtype">
|
||||
<option t-att-value="subtype['id']"><t t-raw="subtype['name']"/></option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</t>
|
||||
<!-- New message composer -->
|
||||
<t t-if="!widget.options.is_log">
|
||||
<!-- Text for followers -->
|
||||
<span class="o_chatter_composer_info">
|
||||
To: Followers of
|
||||
<t t-if="widget.options.record_name">
|
||||
<t t-raw="'"' + widget.options.record_name + '"'"/>
|
||||
</t>
|
||||
<t t-if="!widget.options.record_name">
|
||||
this document
|
||||
</t>
|
||||
</span>
|
||||
<!-- List of followers -->
|
||||
<div class="o_composer_suggested_partners">
|
||||
<t t-foreach='widget.suggested_partners' t-as='recipient'>
|
||||
<div t-attf-title="Add as recipient and follower (reason: #{recipient.reason})">
|
||||
<input type="checkbox"
|
||||
t-att-checked="recipient.checked ? 'checked' : undefined"
|
||||
t-att-data-fullname="recipient.full_name"/>
|
||||
<t t-esc="recipient.name"/>
|
||||
<t t-if="recipient.email_address">(<t t-esc="recipient.email_address"/>)</t>
|
||||
<t t-if="!recipient.email_address">(no email address)</t>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!-- Replace the input by a textarea -->
|
||||
<t t-jquery=".o_composer_input" t-operation="replace">
|
||||
<textarea class="o_composer_input" placeholder="Write something"/>
|
||||
</t>
|
||||
|
||||
<!-- Add a button to open the full composer dialog -->
|
||||
<t t-jquery=".o_composer_buttons" t-operation="append">
|
||||
<button class="btn btn-sm btn-icon fa fa-pencil-square-o o_composer_button_full_composer" type="button"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
Chatter main div
|
||||
-->
|
||||
<t t-name="mail.Chatter">
|
||||
<div class="o_chatter">
|
||||
<div class="o_chatter_topbar">
|
||||
<button type="button" class="btn btn-sm btn-primary o_chatter_button_new_message" title="Send a message">
|
||||
New message
|
||||
</button>
|
||||
<button t-if="widget.options.display_log_button" class="btn btn-sm btn-link o_chatter_button_log_note" title="Log a note. Followers will not be notified.">
|
||||
Log an internal note
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
Followers: main buttons
|
||||
Template used to display the followers, the actions and the subtypes in a record.
|
||||
-->
|
||||
<t t-name="mail.Followers">
|
||||
<div class="o_followers">
|
||||
<div t-attf-class="o_followers_title_box">
|
||||
<button t-attf-class="btn btn-sm btn-default dropdown-toggle"
|
||||
data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="o_followers_count"/>
|
||||
<span class="caret"/>
|
||||
</button>
|
||||
<ul class="dropdown-menu o_followers_list" role="menu"></ul>
|
||||
</div>
|
||||
<div class="o_followers_actions">
|
||||
<div t-attf-class="btn-group btn-group-sm o_subtypes_list">
|
||||
<button class="btn btn-sm btn-primary o_followers_follow_button o_followers_notfollow">
|
||||
<span class="o_follow">Follow</span>
|
||||
<span class="fa fa-times o_followers_unfollow"/>
|
||||
<span class="o_followers_unfollow"> Unfollow</span>
|
||||
<span class="fa fa-check o_followers_following"/>
|
||||
<span class="o_followers_following"> Following</span>
|
||||
</button><button t-attf-class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="caret"/>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
Followers: Partner
|
||||
Template used to display a partner following the record
|
||||
-->
|
||||
<t t-name="mail.Followers.partner">
|
||||
<li class='o_partner'>
|
||||
<img t-att-src="record.avatar_url"/>
|
||||
<a class="o_mail_redirect"
|
||||
t-attf-href="#"
|
||||
t-att-title="record.name"
|
||||
t-att-data-oe-model="record.res_model"
|
||||
t-att-data-oe-id="record.res_id"><t t-esc="record.name"/></a>
|
||||
<i t-if="record.is_editable and (widget.records_length > 1)"
|
||||
class="fa fa-pencil o_edit_subtype hide" title="Edit subscription"
|
||||
t-att-data-id="record.id"
|
||||
t-att-data-oe-id="record.res_id"
|
||||
t-att-data-oe-model="record.res_model"/>
|
||||
<i t-if="widget.view_is_editable" class="fa fa-remove o_remove_follower"
|
||||
title="Remove this follower"/>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
Followers: 'Add followers' and 'Add channels' buttons
|
||||
-->
|
||||
<t t-name="mail.Followers.add_more">
|
||||
<t t-if="widget.view_is_editable">
|
||||
<li class="o_add_follower">
|
||||
<a href="#"> Add Followers </a>
|
||||
</li>
|
||||
<li class="o_add_follower_channel">
|
||||
<a href="#"> Add Channels </a>
|
||||
</li>
|
||||
<li t-if="widget.followers.length > 0" class="divider"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
Followers: Subtype
|
||||
Template used to display message subtypes of a follower subscription
|
||||
-->
|
||||
<t t-name="mail.Followers.subtype">
|
||||
<li class="o_subtype">
|
||||
<input type="checkbox" t-att-checked="record.followed"
|
||||
t-att-id="'input_mail_followers_subtype_'+record.id+(dialog ? '_in_dialog': '')"
|
||||
t-att-data-id="record.id" t-att-name="record.name" class="o_subtype_checkbox"/>
|
||||
<span t-att-for="'input_mail_followers_subtype_'+record.id+(dialog ? '_in_dialog': '')">
|
||||
<t t-raw="record.name"/>
|
||||
</span>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<div t-name="mail.ChatComposer" class="o_chat_composer">
|
||||
<div class="o_composer">
|
||||
<div class="dropup o_composer_mention_dropdown"/>
|
||||
<input type="text" class="o_composer_input" placeholder="Write something"/>
|
||||
<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-icon fa fa-smile-o o_composer_button_emoji" type="button" data-toggle="popover"/>
|
||||
<button class="btn btn-sm btn-icon fa fa-paperclip o_composer_button_add_attachment" type="button"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_composer_mentioned_partners"/>
|
||||
<div class="o_composer_attachments_list"/>
|
||||
|
||||
<span class="hide">
|
||||
<t t-call="HiddenInputFile">
|
||||
<t t-set="fileupload_id" t-value="widget.fileupload_id"/>
|
||||
<t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t>
|
||||
<input type="hidden" name="model" value="mail.compose.message"/>
|
||||
<input type="hidden" name="id" value="0"/>
|
||||
<input type="hidden" name="session_id" t-att-value="widget.session.session_id"/>
|
||||
</t>
|
||||
</span>
|
||||
<span class="o_composer_emoji"></span>
|
||||
</div>
|
||||
|
||||
<t t-name="mail.ChatComposer.emojis">
|
||||
<t t-foreach="emojis" t-as="emoji">
|
||||
<span t-att-data-emoji="emoji.source" class="o_mail_emoji" t-att-title="emoji.description">
|
||||
<t t-raw="emoji.substitution"/>
|
||||
</span>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.ChatComposer.MentionMenu">
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownMailMentionMenu">
|
||||
<t t-foreach="partners" t-as="partner">
|
||||
<li t-att-class="'o_mention_proposition ' + (partner_index === 0 ? 'active' : '')"
|
||||
t-att-data-partner-id="partner.id">
|
||||
<a href="#">
|
||||
<t t-esc="partner.name"/> <t t-if="partner.email">(<t t-esc="partner.email"/>)</t>
|
||||
</a>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.ChatComposer.MentionTags">
|
||||
<t t-if="partners.length">Mention(s): </t>
|
||||
<t t-foreach="partners" t-as="partner">
|
||||
<span class="badge"><t t-esc="partner.name"/></span>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,122 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.ChatThread">
|
||||
<t t-if="messages.length">
|
||||
<t t-if="options.display_load_more && options.display_order == ORDER.ASC"
|
||||
t-call="mail.ChatThread.LoadMore"/>
|
||||
<t t-call="mail.ChatThread.Content"/>
|
||||
<t t-if="options.display_load_more && options.display_order == ORDER.DESC"
|
||||
t-call="mail.ChatThread.LoadMore"/>
|
||||
</t>
|
||||
<t t-if="!messages.length && options.no_content_helper">
|
||||
<div class="o_chat_help_message">
|
||||
<t t-raw="options.no_content_helper"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.ChatThread.Content">
|
||||
<t t-set="current_day" t-value="0"/>
|
||||
<t t-foreach="messages" t-as="message">
|
||||
<div t-if="current_day != message.date.split(' ')[0]" class="o_thread_date_separator">
|
||||
<span class="o_thread_date">
|
||||
<t t-raw="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('LL')"/>
|
||||
</span>
|
||||
<t t-set="current_day" t-value="message.date.split(' ')[0]"/>
|
||||
</div>
|
||||
|
||||
<t t-call="mail.ChatThread.Message"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.ChatThread.Message">
|
||||
<div class="o_thread_message" t-att-data-message-id="message.id">
|
||||
<div t-if="options.display_avatar" class="o_thread_message_avatar">
|
||||
<img t-if="message.avatar_src" t-att-src="message.avatar_src" class="o_thread_message_avatar_img"/>
|
||||
</div>
|
||||
<div class="o_thread_message_core">
|
||||
<span class="pull-right">
|
||||
<i t-if="message.is_needaction && options.display_needactions" class="fa fa-check o_thread_message_needaction" t-att-data-message-id="message.id" t-att-title="_t('Mark as Done')"/>
|
||||
<i t-att-class="'fa fa-lg o_thread_message_star ' + (message.is_starred ? 'fa-star' : 'fa-star-o')" t-att-data-message-id="message.id" t-att-title="_t('Mark as Todo')"/>
|
||||
</span>
|
||||
<p class="text-muted">
|
||||
<t t-if="message.is_note">
|
||||
Note by
|
||||
</t>
|
||||
<t t-if="message.author_id && message.author_id[1]">
|
||||
<strong class="o_mail_redirect" data-oe-model="res.partner" t-att-data-oe-id="message.author_id[0]"><t t-esc="message.author_id[1]"/></strong>
|
||||
</t>
|
||||
<t t-if="!message.author_id || !message.author_id[1]">
|
||||
<strong class="o_mail_redirect"><t t-esc="message.displayed_author"/></strong>
|
||||
</t>
|
||||
|
||||
<small t-att-title="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('LLL')">
|
||||
- <t t-esc="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('hh:mm')"/>
|
||||
</small>
|
||||
<t t-if="message.model && (message.model != 'mail.channel') && options.display_document_link">
|
||||
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>
|
||||
</p>
|
||||
<t t-if="message.tracking_value_ids && message.tracking_value_ids.length > 0">
|
||||
<t t-call="mail.ChatThread.MessageTracking"/>
|
||||
</t>
|
||||
<div t-if="message.subject" class="o_mail_subject">Subject: <t t-esc="message.subject"/></div>
|
||||
<t t-if="!(message.tracking_value_ids && message.tracking_value_ids.length > 0)">
|
||||
<t t-raw="message.body"/>
|
||||
</t>
|
||||
<t t-foreach="message.attachment_ids" t-as="attachment">
|
||||
<t t-call="mail.Attachment"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.ChatThread.MessageTracking">
|
||||
<ul class="o_mail_thread_message_tracking">
|
||||
<t t-foreach='message.tracking_value_ids' t-as='value'>
|
||||
<li>
|
||||
<t t-esc="value.changed_field"/>:
|
||||
<t t-if="value.old_value">
|
||||
<span> <t t-esc="value.old_value"/> </span>
|
||||
<span class="fa fa-long-arrow-right"/>
|
||||
</t>
|
||||
<span> <t t-esc="value.new_value"/> </span>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.ChatComposer.Attachments">
|
||||
<div class="o_attachments">
|
||||
<t t-foreach="attachments" t-as='attachment'>
|
||||
<t t-call="mail.Attachment">
|
||||
<t t-set="editable" t-value="true"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.Attachment">
|
||||
<div t-attf-class="o_attachment #{attachment.upload ? 'o_attachment_uploading' : ''}" t-att-title="attachment.name">
|
||||
<a class="o_image" t-att-href='attachment.url' target="_blank" t-att-data-mimetype="attachment.mimetype" t-attf-data-src="/web/image/#{attachment.id}/100x80">
|
||||
<span class='o_attachment_name'><t t-raw='attachment.name'/></span>
|
||||
</a>
|
||||
<t t-if="editable">
|
||||
<div class="o_attachment_delete">
|
||||
<i class='fa fa-times-circle' title="Delete this attachment" t-att-data-id="attachment.id"/>
|
||||
</div>
|
||||
<div class="o_attachment_progress_bar">
|
||||
Uploading
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.ChatThread.LoadMore">
|
||||
<div t-if="!all_messages_loaded" class="o_thread_show_more">
|
||||
<button class="btn btn-sm btn-link">-------- Show more messages --------</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
Reference in New Issue
Block a user