[REF] mail: last part of the chat refactoring (client side)
Removal of old files and update of assets and imports Joint work with aab
This commit is contained in:
@@ -58,11 +58,12 @@ Main Features
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'qweb': [
|
||||
'static/src/xml/mail_thread.xml',
|
||||
'static/src/xml/mail_chatter.xml',
|
||||
'static/src/xml/mail_chat_common.xml',
|
||||
'static/src/xml/mail_chat_backend.xml',
|
||||
'static/src/xml/mail_followers.xml',
|
||||
'static/src/xml/client_action.xml',
|
||||
'static/src/xml/composer.xml',
|
||||
'static/src/xml/chatter.xml',
|
||||
'static/src/xml/systray.xml',
|
||||
'static/src/xml/thread.xml',
|
||||
'static/src/xml/chat_window.xml',
|
||||
'static/src/xml/announcement.xml',
|
||||
],
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,412 +0,0 @@
|
||||
odoo.define('mail.chat_common', function (require) {
|
||||
"use strict";
|
||||
|
||||
var bus = require('bus.bus');
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
var time = require('web.time');
|
||||
var Widget = require('web.Widget');
|
||||
var mail_utils = require('mail.utils');
|
||||
|
||||
var _t = core._t;
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var NBR_LIMIT_HISTORY = 20;
|
||||
|
||||
|
||||
/**
|
||||
* Widget handeling the sessions/conversations
|
||||
*
|
||||
* Responsible to listen the bus and do the correct action according to the received notifications (fetch
|
||||
* conversation, add message, ...)
|
||||
**/
|
||||
var ConversationManager = Widget.extend({
|
||||
init: function(parent, options) {
|
||||
this._super(parent);
|
||||
this.options = _.clone(options) || {};
|
||||
_.defaults(this.options, {
|
||||
placeholder: _t("Say something..."),
|
||||
default_username: _t("Anonymous"),
|
||||
load_history: true,
|
||||
focus: false,
|
||||
});
|
||||
this.emoji_list = [];
|
||||
// business
|
||||
this.sessions = {};
|
||||
this.bus = bus.bus;
|
||||
this.bus.on("notification", this, this.on_notification);
|
||||
// ui
|
||||
this.set("right_offset", 0);
|
||||
this.set("bottom_offset", 0);
|
||||
this.on("change:right_offset", this, this.calc_positions);
|
||||
this.on("change:bottom_offset", this, this.calc_positions);
|
||||
|
||||
this.set("waiting_messages", 0);
|
||||
this.on("change:waiting_messages", this, this.window_title_change);
|
||||
$(window).on("focus", _.bind(this.window_focus, this));
|
||||
this.window_title_change();
|
||||
// session widgets deferred (for their creation)
|
||||
this._session_defs = {};
|
||||
},
|
||||
start: function(){
|
||||
var self = this;
|
||||
return $.when(session.rpc("/mail/chat_init"), this._super.apply(this, arguments)).then(function(result){
|
||||
self._setup(result);
|
||||
});
|
||||
},
|
||||
_setup: function(init_data){
|
||||
this.emoji_list = init_data.emoji;
|
||||
this.options.emoji = mail_utils.shortcode_substitution(init_data.emoji);
|
||||
},
|
||||
on_notification: function(notification, options) {
|
||||
var self = this;
|
||||
var channel = notification[0];
|
||||
var message = notification[1];
|
||||
var regex_uuid = new RegExp(/(\w{8}(-\w{4}){3}-\w{12}?)/g);
|
||||
var is_channel_uuid = regex_uuid.test(channel);
|
||||
|
||||
// (dbname, 'res.partner', partner_id) receive only the new mail.channel header
|
||||
if((Array.isArray(channel) && (channel[1] === 'res.partner')) || is_channel_uuid){
|
||||
// activate the received session
|
||||
if(message.uuid){
|
||||
this.session_apply(message, options);
|
||||
}
|
||||
}
|
||||
// (dbname, 'mail.channel', channel_id) receive only the mail.message of the channel
|
||||
if((Array.isArray(channel) && (channel[1] === 'mail.channel')) || is_channel_uuid){
|
||||
// message to display in the chatview
|
||||
if (message.message_type) {
|
||||
self.message_receive(message);
|
||||
}
|
||||
}
|
||||
},
|
||||
// window focus unfocus beep and title
|
||||
window_focus: function() {
|
||||
this.set("waiting_messages", 0);
|
||||
},
|
||||
window_beep: function() {
|
||||
mail_utils.beep(session);
|
||||
},
|
||||
window_title_change: function() {
|
||||
if (this.get("waiting_messages") !== 0) {
|
||||
this.window_beep();
|
||||
}
|
||||
},
|
||||
// sessions methods
|
||||
session_apply: function(session, options){
|
||||
// options used by this function : focus (load_history can be usefull)
|
||||
var self = this;
|
||||
options = _.defaults(options || {}, {
|
||||
'focus': true,
|
||||
}, this.options);
|
||||
// create/get the conversation widget
|
||||
var conv = this.sessions[session.id];
|
||||
if (! conv) {
|
||||
if(session.state !== 'closed'){
|
||||
conv = new Conversation(this, session, options);
|
||||
// store this deferred to apply next action (notifications) when it is resolved.
|
||||
this._session_defs[session.id] = conv.appendTo($("body"));
|
||||
this._session_defs[session.id].then(function() {
|
||||
self.calc_positions();
|
||||
});
|
||||
conv.on("destroyed", this, _.bind(this.session_delete, this));
|
||||
this.sessions[session.id] = conv;
|
||||
}
|
||||
}else{
|
||||
// update the state only when the 'contruction' deferred is resolved.
|
||||
this._session_defs[session.id].then(function(){
|
||||
conv.set('session', session);
|
||||
});
|
||||
}
|
||||
return conv;
|
||||
},
|
||||
session_delete: function(channel_id){
|
||||
delete this.sessions[channel_id];
|
||||
this.calc_positions();
|
||||
},
|
||||
/**
|
||||
* Adding a message in all its channels
|
||||
* /!\ Suppose the channel are already loaded in the ConversationManager /!\
|
||||
* @param {Object} message : the received message (formatted server side)
|
||||
*/
|
||||
message_receive: function(message){
|
||||
var self = this;
|
||||
_.each(message.channel_ids, function(channel_id){
|
||||
self.sessions[channel_id].message_receive(message);
|
||||
});
|
||||
},
|
||||
// others
|
||||
calc_positions: function() {
|
||||
var self = this;
|
||||
var current = this.get("right_offset");
|
||||
_.each(this.sessions, function(s) {
|
||||
s.set("bottom_position", self.get("bottom_offset"));
|
||||
s.set("right_position", current);
|
||||
current += s.$().outerWidth(true);
|
||||
});
|
||||
},
|
||||
destroy: function() {
|
||||
$(window).off("focus", this.window_focus);
|
||||
$(window).off("blur", this.window_blur);
|
||||
return this._super();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Widget Conversation
|
||||
*
|
||||
* Display the conversation in a 'portable' window, manage sending and receiving message,
|
||||
* and the fold state of the conversation.
|
||||
**/
|
||||
var Conversation = Widget.extend({
|
||||
template: "mail.chat.im.Conversation",
|
||||
events: {
|
||||
"keydown input": "keydown",
|
||||
"click .o_mail_chat_im_window_close": "on_click_close",
|
||||
"click .o_mail_chat_im_window_header": "on_click_header"
|
||||
},
|
||||
init: function(parent, session, options) {
|
||||
this._super(parent);
|
||||
this.options = _.defaults(options || {}, {
|
||||
'emoji': {},
|
||||
'default_username': _t('Anonymous'),
|
||||
'placeholder': _t('Write something ...'),
|
||||
'loading_history': true,
|
||||
'focus': true,
|
||||
});
|
||||
this.set("messages", []);
|
||||
this.set("session", session);
|
||||
this.set("pending", 0);
|
||||
this.set("right_position", 0);
|
||||
this.set("bottom_position", 0);
|
||||
},
|
||||
willStart: function(){
|
||||
if(this.options.loading_history){
|
||||
return $.when(this.message_history(), this._super.apply(this, arguments));
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
start: function() {
|
||||
var self = this;
|
||||
// ui
|
||||
self.on("change:right_position", self, self.calc_pos);
|
||||
self.on("change:bottom_position", self, self.calc_pos);
|
||||
self.on("change:pending", self, self.message_pending_change);
|
||||
|
||||
self.full_height = self.$().height();
|
||||
self.$('.o_mail_chat_im_window_content').on('scroll',function(){
|
||||
if($(this).scrollTop() === 0){
|
||||
self.message_history();
|
||||
}
|
||||
});
|
||||
// business
|
||||
self.on("change:session", self, self.session_change);
|
||||
self.on("change:messages", self, self.message_render);
|
||||
|
||||
return self._super.apply(this, arguments).then(function(){
|
||||
// place the conversation window correctly on the screen
|
||||
self.calc_pos();
|
||||
// focus if needed
|
||||
if(self.options.focus){
|
||||
self.focus();
|
||||
}
|
||||
// re-apply the session to display the correct state
|
||||
self.set('session', _.clone(self.get('session')));
|
||||
self._go_bottom();
|
||||
});
|
||||
},
|
||||
// ui
|
||||
show: function(){
|
||||
this.$().animate({
|
||||
height: this.full_height
|
||||
});
|
||||
this.set("pending", 0);
|
||||
},
|
||||
hide: function(){
|
||||
this.$().animate({
|
||||
height: this.$(".o_mail_chat_im_window_header").outerHeight()
|
||||
});
|
||||
},
|
||||
calc_pos: function() {
|
||||
this.$().css("right", this.get("right_position"));
|
||||
this.$().css("bottom", this.get("bottom_position"));
|
||||
},
|
||||
// session
|
||||
/**
|
||||
* Set the fold state LOCALLY for the session. This will
|
||||
* trigger a ui update
|
||||
* @param {string} state : the fold state ('fold', 'closed' or 'open')
|
||||
*/
|
||||
session_fold: function(state){
|
||||
var session = this.get('session');
|
||||
session.state = state;
|
||||
this.set('session', session);
|
||||
},
|
||||
/**
|
||||
* Triggered when the session has changed : it update the header name
|
||||
* and fold the window (ui) according to its state.
|
||||
*/
|
||||
session_change: function(){
|
||||
// update conversation name in header
|
||||
var name = this.get("session").name || _t('Unknown Channel');
|
||||
this.$(".o_mail_chat_im_window_header_name").text(name);
|
||||
this.$(".o_mail_chat_im_window_header_name").attr('title', name);
|
||||
// update the fold state
|
||||
if(this.get("session").state){
|
||||
if(this.get("session").state === 'closed'){
|
||||
this.destroy();
|
||||
}else{
|
||||
if(this.get("session").state === 'open'){
|
||||
this.show();
|
||||
}else{
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// messages
|
||||
/**
|
||||
* Load the history of the current session, and insert it into already loaded messages
|
||||
* @returns {Deferred} resolved with the new loaded messages (Object[])
|
||||
*/
|
||||
message_history: function(){
|
||||
var self = this;
|
||||
if(this.options.loading_history){
|
||||
var data = {
|
||||
uuid: self.get("session").uuid,
|
||||
limit: NBR_LIMIT_HISTORY,
|
||||
last_id: _.first(this.get("messages")) ? _.first(this.get("messages")).id : false,
|
||||
};
|
||||
return session.rpc("/mail/chat_history", data).then(function(messages){
|
||||
if(messages){
|
||||
self.message_insert(messages);
|
||||
if(messages.length != NBR_LIMIT_HISTORY){
|
||||
self.options.loading_history = false;
|
||||
}
|
||||
}else{
|
||||
self.options.loading_history = false;
|
||||
}
|
||||
return messages;
|
||||
});
|
||||
}
|
||||
return $.Deferred().resolve([]);
|
||||
},
|
||||
message_receive: function(message) {
|
||||
// don't add duplicated message (this can happen when refreshing and receiving already recieved
|
||||
// message that are still in the bus queue/buffer).
|
||||
if(!_.contains(_.pluck(this.get('messages'), 'id'), message.id)){
|
||||
// is pending message ?
|
||||
if (this.get('session').state === 'open') {
|
||||
this.set("pending", 0);
|
||||
} else {
|
||||
if(!this.message_am_i_author(message)){
|
||||
this.set("pending", this.get("pending") + 1);
|
||||
}
|
||||
}
|
||||
// insert it
|
||||
this.message_insert([message]);
|
||||
this._go_bottom();
|
||||
}
|
||||
},
|
||||
message_send: function(message) {
|
||||
var self = this;
|
||||
var send_it = function() {
|
||||
return session.rpc("/mail/chat_post", {uuid: self.get("session").uuid, message_content: message});
|
||||
};
|
||||
var tries = 0;
|
||||
send_it().fail(function(error, e) {
|
||||
e.preventDefault();
|
||||
tries += 1;
|
||||
if (tries < 3)
|
||||
return send_it();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Preporcess the message (add informations in the Object, ...)
|
||||
* @param {Object[]} messages : server side formatted message (from message_format method)
|
||||
*/
|
||||
_message_preprocess: function(raw_messages){
|
||||
var self = this;
|
||||
_.each(raw_messages, function(m){
|
||||
m.date = moment(time.str_to_datetime(m.date)).format('YYYY-MM-DD HH:mm:ss'); // set the date in the correct browser user timezone
|
||||
if(m.body){
|
||||
m.body = mail_utils.shortcode_apply(m.body, self.options.emoji);
|
||||
}
|
||||
_.each(m.attachment_ids, function(a){
|
||||
a.url = '/web/content/' + a.id + '?download=true';
|
||||
a.fa_class = mail_utils.attachment_mimetype_icon(a.mimetype);
|
||||
});
|
||||
});
|
||||
return raw_messages;
|
||||
},
|
||||
/**
|
||||
* Insert new raw message into the current ones
|
||||
* @param {Object[]} messages : server side formatted message (from message_format method)
|
||||
*/
|
||||
message_insert: function(raw_messages){
|
||||
var messages = this._message_preprocess(raw_messages);
|
||||
this.set("messages", _.sortBy(this.get("messages").concat(messages), function(m){ return m.id; }));
|
||||
},
|
||||
message_render: function(){
|
||||
var message_html = $(QWeb.render("mail.chat.im.Conversation.messages", {"widget": this}));
|
||||
this.$('.o_mail_chat_im_window_content').html(message_html);
|
||||
},
|
||||
message_am_i_author: function(message){
|
||||
return message.author_id && message.author_id[0] === session.partner_id;
|
||||
},
|
||||
message_pending_change: function(){
|
||||
if (this.get("pending") === 0) {
|
||||
this.$(".o_mail_chat_im_window_header_counter").text("");
|
||||
} else {
|
||||
this.$(".o_mail_chat_im_window_header_counter").text("(" + this.get("pending") + ")");
|
||||
}
|
||||
},
|
||||
// utils and event
|
||||
_go_bottom: function() {
|
||||
if(this.$(".o_mail_chat_im_window_content").length){
|
||||
this.$(".o_mail_chat_im_window_content").scrollTop(this.$(".o_mail_chat_im_window_content").get(0).scrollHeight);
|
||||
}
|
||||
},
|
||||
focus: function() {
|
||||
this.$(".o_mail_chat_im_window_input").focus();
|
||||
},
|
||||
keydown: function(e) {
|
||||
if(e && e.which == 27) {
|
||||
if(this.$el.prev().find('.o_mail_chat_im_window_input').length > 0){
|
||||
this.$el.prev().find('.o_mail_chat_im_window_input').focus();
|
||||
}else{
|
||||
this.$el.next().find('.o_mail_chat_im_window_input').focus();
|
||||
}
|
||||
e.stopPropagation();
|
||||
this.session_fold('closed');
|
||||
}
|
||||
if(e && e.which !== 13) {
|
||||
return;
|
||||
}
|
||||
var mes = this.$("input").val();
|
||||
if (! mes.trim()) {
|
||||
return;
|
||||
}
|
||||
this.$("input").val("");
|
||||
this.message_send(mes);
|
||||
},
|
||||
on_click_header: function(){
|
||||
this.session_fold();
|
||||
},
|
||||
on_click_close: function(event) {
|
||||
event.stopPropagation(); // avoid triggering event for 'on_click_header'
|
||||
this.session_fold('closed');
|
||||
},
|
||||
destroy: function() {
|
||||
this.trigger("destroyed", this.get('session').id);
|
||||
return this._super();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
Conversation: Conversation,
|
||||
ConversationManager: ConversationManager,
|
||||
};
|
||||
|
||||
});
|
||||
@@ -1,459 +0,0 @@
|
||||
odoo.define('mail.chatter', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var web_client = require('web.web_client');
|
||||
var Widget = require('web.Widget');
|
||||
var form_common = require('web.form_common');
|
||||
var SystrayMenu = require('web.SystrayMenu');
|
||||
var mail_utils = require('mail.utils');
|
||||
var mail_thread = require('mail.thread');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
var internal_bus = core.bus;
|
||||
|
||||
|
||||
/**
|
||||
* Mail Compose Message for Chatter
|
||||
*
|
||||
* Extends the basic MailComposeMessage Widget to add 'suggested partner' layer (open popup
|
||||
* when suggested partner is selected without email, or other informations), and the button
|
||||
* to open the 'mail.compose.message' wizard.
|
||||
**/
|
||||
var ChatterMailComposeMessage = mail_thread.MailComposeMessage.extend({
|
||||
template: 'mail.chatter.ComposeMessage',
|
||||
init: function(parent, dataset, options){
|
||||
this._super.apply(this, arguments);
|
||||
this.suggested_partners = [];
|
||||
this.context = options.context || {};
|
||||
this.options = _.defaults(options, {
|
||||
'record_name': false,
|
||||
'is_log': false,
|
||||
'internal_subtypes': [],
|
||||
});
|
||||
this.events = _.extend(this.events, {
|
||||
'click .o_mail_compose_message_button_full_message': 'on_compose_message',
|
||||
});
|
||||
},
|
||||
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(){
|
||||
this.$input = this.$('.o_mail_compose_message_input');
|
||||
autosize(this.$input);
|
||||
this.$input.focus();
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
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 = mail_utils.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 partner
|
||||
* @returns Array() : list of 'recipient' selected partner (may not be created in db)
|
||||
**/
|
||||
get_checked_suggested_partners: function(){
|
||||
var self = this;
|
||||
var checked_partners = [];
|
||||
this.$('.o_mail_chatter_compose_message_suggested_partners input:checked').each(function(){
|
||||
var $input = $(this);
|
||||
var full_name = $input.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 = mail_utils.parse_email(partner_name);
|
||||
|
||||
var pop = new form_common.FormViewDialog(this, {
|
||||
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();
|
||||
pop.on('closed', self, function () {
|
||||
deferred.resolve();
|
||||
});
|
||||
pop.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;
|
||||
},
|
||||
message_post: function(body, attachment_ids, values){
|
||||
var self = this;
|
||||
var _super = self._super;
|
||||
// default values
|
||||
values = _.defaults(values || {}, {
|
||||
'message_type': 'comment',
|
||||
'content_subtype': 'html',
|
||||
'partner_ids': [],
|
||||
'subtype_id': false,
|
||||
'subtype': false,
|
||||
});
|
||||
// update subtype
|
||||
if(this.options.is_log) {
|
||||
var subtype_id = parseInt(this.$('.o_mail_chatter_compose_message_subtype_select').val());
|
||||
if(_.indexOf(_.pluck(this.options.internal_subtypes, 'id'), subtype_id) == -1) {
|
||||
values.subtype = 'mail.mt_note';
|
||||
}else{
|
||||
values.subtype_id = subtype_id;
|
||||
}
|
||||
}else{
|
||||
values.subtype = 'mail.mt_comment';
|
||||
}
|
||||
// update partner_ids
|
||||
var def = $.when();
|
||||
if(this.options.is_log){
|
||||
// directly call the super()
|
||||
def = _.bind(_super, self)(body, attachment_ids, values);
|
||||
}else{
|
||||
// call the super() when all partner popup are treated
|
||||
var checked_suggested_partners = this.get_checked_suggested_partners();
|
||||
this.check_suggested_partners(checked_suggested_partners).done(function(partner_ids){
|
||||
// update context
|
||||
values.partner_ids = partner_ids;
|
||||
values.context = _.defaults(self.context, {
|
||||
'mail_post_autofollow': true,
|
||||
'mail_post_autofollow_partner_ids': partner_ids,
|
||||
});
|
||||
def = _.bind(_super, self)(body, attachment_ids, values);
|
||||
});
|
||||
}
|
||||
return def;
|
||||
},
|
||||
is_empty: function(){
|
||||
return this.$input.val() === '';
|
||||
},
|
||||
on_compose_message: function(){
|
||||
var self = this;
|
||||
if (!this.do_check_attachment_upload()){
|
||||
return false;
|
||||
}
|
||||
|
||||
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': mail_utils.get_text2html(self.$('.o_mail_compose_message_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)
|
||||
*
|
||||
* Displayed on every document. Since it is displayed on a form view, it extends 'AbstractField' widget, and
|
||||
* required the behaviour of 'MailThreadMixin' to manage mail.message (using common methods).
|
||||
* This widget is responsible of the toggle the message composer and the custom message rendering.
|
||||
**/
|
||||
var ChatterMailThread = form_common.AbstractField.extend(mail_thread.MailThreadMixin, {
|
||||
template: 'mail.chatter.ChatterMailThread',
|
||||
events: {
|
||||
"click .o_mail_redirect": "on_click_redirect",
|
||||
"click .o_mail_thread_message_star": "on_message_star",
|
||||
"click .o_mail_thread_message_needaction": "on_message_needaction",
|
||||
"click .o_mail_thread_show_more": "message_load_history",
|
||||
"click .o_mail_expand": "on_message_body_toggle",
|
||||
// toggle message composer (!! declaration order is important !!)
|
||||
"click .o_mail_chatter_button_new_message": "on_open_composer",
|
||||
"click .o_mail_chatter_button_log_note": "on_open_composer",
|
||||
"blur .o_mail_compose_message_input": "on_close_composer",
|
||||
"mousedown .o_mail_chatter_compose_message": function() {
|
||||
this.stay_open = true;
|
||||
},
|
||||
"focus .o_mail_compose_message_input": function() {
|
||||
this.stay_open = false;
|
||||
},
|
||||
"keydown .o_mail_compose_message_input": function() {
|
||||
this.stay_open = false;
|
||||
},
|
||||
},
|
||||
init: function (){
|
||||
this._super.apply(this, arguments);
|
||||
mail_thread.MailThreadMixin.init.call(this);
|
||||
this.message_composer = undefined;
|
||||
this.record_name = '';
|
||||
this.context = this.options.context || {};
|
||||
// options from 'message_ids' node in field_view_get
|
||||
this.options = _.defaults(this.options, {
|
||||
'display_log_button': true,
|
||||
'display_document_link': false,
|
||||
'internal_subtypes': [],
|
||||
'emoji_list': [],
|
||||
});
|
||||
this.emoji_set_substitution(this.options.emoji_list);
|
||||
},
|
||||
start: function(){
|
||||
mail_thread.MailThreadMixin.start.call(this);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* 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_message_post: function(message_id){
|
||||
var self = this;
|
||||
return this.message_format([message_id]).then(function(messages){
|
||||
self.stay_open = false;
|
||||
self.message_insert(messages);
|
||||
self.on_close_composer();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 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(){
|
||||
var self = this;
|
||||
// re-rendering for mail composer message
|
||||
this.all_messages_loaded = this.get_value().length <= mail_thread.LIMIT_MESSAGE;
|
||||
this.renderElement();
|
||||
// reset dataset
|
||||
this.ThreadDataset = this.view.dataset;
|
||||
// update context (require for mail.compose.message wizard)
|
||||
this.context = _.extend({
|
||||
'mail_read_set_read': true, // set messages as read in Chatter TODO JEM : not used anymore
|
||||
'default_res_id': this.view.datarecord.id || false,
|
||||
'default_model': this.view.model || false,
|
||||
}, this.options.context || {});
|
||||
// fetch messages of current document
|
||||
var ids = this.get_value() || [];
|
||||
return this.message_format(ids.slice(0, mail_thread.LIMIT_MESSAGE)).then(function(raw_messages){
|
||||
self._message_replace(self._message_preprocess(raw_messages));
|
||||
self.record_name = _.last(raw_messages) ? _.last(raw_messages).record_name : '';
|
||||
// set the thread as read
|
||||
self.ThreadDataset.call('message_set_read', [[self.view.datarecord.id]]).then(function(message_ids){
|
||||
// decrement the needaction top counter
|
||||
internal_bus.trigger('mail_needaction_done', message_ids.length);
|
||||
});
|
||||
});
|
||||
},
|
||||
// composer toggle
|
||||
on_open_composer: function(event){
|
||||
var self = this;
|
||||
this.$('.o_mail_chatter_compose_message').show();
|
||||
// destroy existing composer
|
||||
if(this.message_composer){
|
||||
this.message_composer.destroy();
|
||||
}
|
||||
// create the new composer
|
||||
this.message_composer = new ChatterMailComposeMessage(this, this.ThreadDataset, {
|
||||
'internal_subtypes': this.options.internal_subtypes,
|
||||
'is_log': this.$(event.currentTarget).hasClass('o_mail_chatter_button_log_note'),
|
||||
'emoji_list': this.options.emoji_list,
|
||||
'context': this.context,
|
||||
'record_name': this.record_name,
|
||||
});
|
||||
this.message_composer.appendTo(this.$('.o_mail_chatter_compose_message'));
|
||||
this.message_composer.on('message_sent', this, this.on_message_post);
|
||||
this.message_composer.on('need_refresh', this, function(){
|
||||
self.message_refresh();
|
||||
self.on_close_composer();
|
||||
});
|
||||
},
|
||||
on_close_composer: function(){
|
||||
if(this.message_composer && this.message_composer.is_empty() && !this.stay_open){
|
||||
this.$('.o_mail_chatter_compose_message').hide();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Refresh the current message, and set them as current message. Use 'message_fetch', since its
|
||||
* domain don't use 'ids', so if there is new messages, they'll be fetched.
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
message_refresh: function(){
|
||||
return this.message_load_new();
|
||||
},
|
||||
/**
|
||||
* Render the messages
|
||||
* @override
|
||||
*/
|
||||
message_render: function(){
|
||||
this.$('.o_mail_chatter_messages').html(QWeb.render('mail.chatter.ChatterMailThread.messages', {'widget': this}));
|
||||
|
||||
// display image thumbnail
|
||||
this.$(".o_mail_chatter_messages .o_image[data-mimetype^='image']").each(function () {
|
||||
var $img = $(this);
|
||||
if (/gif|jpe|jpg|png/.test($img.data('mimetype')) && $img.data('src')) {
|
||||
$img.css('background-image', "url('" + $img.data('src') + "')");
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Order the messages
|
||||
* @param {Object[]} messages : messages to order
|
||||
* @override
|
||||
*/
|
||||
_message_order: function(messages){
|
||||
messages = mail_thread.MailThreadMixin._message_order.call(this, messages);
|
||||
return messages.reverse();
|
||||
},
|
||||
/**
|
||||
* Return the message domain for the current document mail thread
|
||||
* @override
|
||||
*/
|
||||
get_message_domain: function(){
|
||||
var res_id = this.ThreadDataset.ids[this.ThreadDataset.index];
|
||||
return [['model', '=', this.ThreadDataset.model], ['res_id', '=', res_id]];
|
||||
}
|
||||
});
|
||||
|
||||
core.form_widget_registry.add('mail_thread', ChatterMailThread);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Global ComposeMessage Top Button
|
||||
*
|
||||
* Add a link on the top user bar to write a full mail. It opens the form view
|
||||
* of the mail.compose.message (in a modal).
|
||||
*/
|
||||
var ComposeMessageTopButton = Widget.extend({
|
||||
template:'mail.ComposeMessageTopButton',
|
||||
events: {
|
||||
"click": "on_compose_message",
|
||||
},
|
||||
on_compose_message: function (ev) {
|
||||
ev.preventDefault();
|
||||
web_client.action_manager.do_action({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'mail.compose.message',
|
||||
view_mode: 'form',
|
||||
view_type: 'form',
|
||||
views: [[false, 'form']],
|
||||
target: 'new',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Put the ComposeMessageTopButton widget in the systray menu
|
||||
SystrayMenu.Items.push(ComposeMessageTopButton);
|
||||
|
||||
|
||||
return {
|
||||
ChatterMailThread: ChatterMailThread,
|
||||
};
|
||||
|
||||
});
|
||||
@@ -1,424 +0,0 @@
|
||||
odoo.define('mail.mail_followers', function (require) {
|
||||
"use strict";
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* ------------------------------------------------------------
|
||||
* Mail Followers Widget
|
||||
* ------------------------------------------------------------
|
||||
*
|
||||
* This widget handles the display of a list of records as a vertical
|
||||
* list, with an image on the left. The widget itself is a floatting
|
||||
* right-sided box.
|
||||
* This widget is mainly used to display the followers of records
|
||||
* in OpenChatter.
|
||||
*/
|
||||
|
||||
|
||||
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.$('.o_followers_follow_button').on('click', function () {
|
||||
if($(this).hasClass('o_followers_notfollow')) {
|
||||
self.do_follow();
|
||||
}
|
||||
else {
|
||||
self.do_unfollow([session.uid]);
|
||||
}
|
||||
});
|
||||
|
||||
// event: click on a subtype, that (un)subscribe for this subtype
|
||||
this.$el.on('click', '.o_subtypes_list input', function(event) {
|
||||
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.stopPropagation();
|
||||
});
|
||||
|
||||
// event: click on 'invite' button, that opens the invite wizard
|
||||
this.$el.on('click', '.o_add_follower', function(e){
|
||||
e.preventDefault();
|
||||
self.on_invite_follower(false);
|
||||
});
|
||||
this.$el.on('click', '.o_add_follower_channel', function(e){
|
||||
e.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 user_pid = $currentTarget.data('res-id');
|
||||
var follower_id = $currentTarget.data('id');
|
||||
self.dialog = new Dialog(this, {
|
||||
size: 'small',
|
||||
title: _t('Edit Subscription of ') + $currentTarget.siblings('a').text(),
|
||||
buttons: [
|
||||
{
|
||||
text: _t("Apply"),
|
||||
click: function() {
|
||||
self.do_update_subscription(event, user_pid);
|
||||
},
|
||||
close: true
|
||||
},
|
||||
{
|
||||
text: _t("Cancel"),
|
||||
close: true,
|
||||
},
|
||||
],
|
||||
}).open();
|
||||
return self.fetch_subtypes(follower_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('res-model');
|
||||
var res_id = $(event.target).parent().find('a').data('res-id');
|
||||
if (res_model === 'res.partner') { return this.do_unfollow(undefined, [res_id], undefined); }
|
||||
else { return this.do_unfollow(undefined, undefined, [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');
|
||||
});
|
||||
},
|
||||
|
||||
do_unfollow: function (user_ids, partner_ids, channel_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 (partner_ids || channel_ids) {
|
||||
return this.ds_model.call(
|
||||
'message_unsubscribe', [
|
||||
[this.view.datarecord.id],
|
||||
partner_ids,
|
||||
channel_ids,
|
||||
context]
|
||||
).then(this.proxy('read_value'));
|
||||
}
|
||||
else {
|
||||
return this.ds_model.call(
|
||||
'message_unsubscribe_users', [
|
||||
[this.view.datarecord.id],
|
||||
user_ids,
|
||||
context]
|
||||
).then(this.proxy('read_value'));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
do_update_subscription: function (event, user_pid) {
|
||||
var self = this;
|
||||
this.data_subtype = {};
|
||||
|
||||
var kwargs = {};
|
||||
var action_subscribe = 'message_subscribe_users';
|
||||
var subtypes = this.$('.o_followers_actions input[type="checkbox"]');
|
||||
|
||||
// Subtypes edited from the modal
|
||||
if (user_pid) {
|
||||
action_subscribe = 'message_subscribe';
|
||||
subtypes = this.dialog.$('input[type="checkbox"]');
|
||||
kwargs['partner_ids'] = [user_pid];
|
||||
}else{
|
||||
kwargs['user_ids'] = [session.uid];
|
||||
}
|
||||
|
||||
// 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 partner.
|
||||
if (!checklist.length) {
|
||||
if (!this.do_unfollow(undefined, [user_pid], undefined)) {
|
||||
$(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'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/* Add the widget to registry */
|
||||
core.form_widget_registry.add('mail_followers', Followers);
|
||||
|
||||
});
|
||||
@@ -1,823 +0,0 @@
|
||||
odoo.define('mail.thread', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var data = require('web.data');
|
||||
var Model = require('web.Model');
|
||||
var time = require('web.time');
|
||||
var Widget = require('web.Widget');
|
||||
var session = require('web.session');
|
||||
var web_client = require('web.web_client');
|
||||
var mail_utils = require('mail.utils');
|
||||
|
||||
var _t = core._t;
|
||||
var QWeb = core.qweb;
|
||||
|
||||
|
||||
var LIMIT_MESSAGE = 20;
|
||||
|
||||
/**
|
||||
* Widget : Input textbox to post messages
|
||||
*
|
||||
* Input with 2 buttons to manage attachment and emoji
|
||||
* - Attachment : upload selected attachment (one at a time) and display them below the textbox. Prevent
|
||||
posting message while uploading
|
||||
* - Emoji : popover exposing emoji list, and append emoji shortcode to the textbox when click on emoji image
|
||||
* It triggers 'message_sent' event with the posted message, when it is sent to the server.
|
||||
*/
|
||||
var MailComposeMessage = Widget.extend({
|
||||
template: 'mail.ComposeMessage',
|
||||
events: {
|
||||
"keydown .o_mail_compose_message_input": "on_keydown",
|
||||
"keyup .o_mail_compose_message_input": "on_keyup",
|
||||
"keypress .o_mail_compose_message_input": "on_keypress",
|
||||
"change input.o_form_input_file": "on_attachment_change",
|
||||
"click .o_mail_attachment_delete": "on_attachment_delete",
|
||||
"click .o_mail_compose_message_button_attachment": 'on_click_attachment',
|
||||
"click .o_mail_compose_message_button_send": 'message_send',
|
||||
"click .o_mail_mention_proposition": "on_click_mention_item",
|
||||
"mouseover .o_mail_mention_proposition": "on_hover_mention_proposition",
|
||||
},
|
||||
/**
|
||||
* Contructor
|
||||
* @param {Object} options : the options of the MailComposeMessage
|
||||
* @param {Object[]} [options.emoji_list] : the list of emoji
|
||||
* @param {Object} options.context : the context. It should contains the default_res_id' (id of the current document).
|
||||
* @param {chat|mail} options.display_mode : the 'chat' mode will display an input, when the 'mail' mode will display a texarea.
|
||||
* @param {Char} [options.mention_delimiter] : the delimiter char to distinguish the beginning of a mention
|
||||
* @param {Integer} [options.mention_min_length] : min length of the mention word required to start a search on partner
|
||||
* @param {Integer} [options.mention_typing_speed] : delay before starting a search with the mention word
|
||||
* @param {Integer} [options.mention_fetch_limit] : limit of partner fetch
|
||||
*/
|
||||
init: function(parent, dataset, options){
|
||||
this._super.apply(this, arguments);
|
||||
this.thread_dataset = dataset;
|
||||
this.options = _.defaults(options || {}, {
|
||||
'emoji_list': {},
|
||||
'context': {},
|
||||
'display_mode': 'mail',
|
||||
'mention_delimiter': '@',
|
||||
'mention_min_length': 2,
|
||||
'mention_typing_speed': 400,
|
||||
'mention_fetch_limit': 8,
|
||||
});
|
||||
this.emoji_list = this.options.emoji_list;
|
||||
this.context = this.options.context;
|
||||
this.input_buffer = ''; // save the value at keydown
|
||||
// attachment handeling
|
||||
this.AttachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context);
|
||||
this.fileupload_id = _.uniqueId('o_mail_chat_fileupload');
|
||||
this.set('attachment_ids', []);
|
||||
// mention
|
||||
this.PartnerModel = new Model('res.partner');
|
||||
this.set('mention_word', false); // word typed after the delimiter
|
||||
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_mail_compose_message_button_attachment");
|
||||
this.$attachments_list = this.$el.filter('.o_mail_attachments_list');
|
||||
this.$mention_partner_tags = this.$el.filter('.o_mail_mention_partner_tags');
|
||||
this.$mention_dropdown = this.$('.o_mail_mention_dropdown');
|
||||
this.$input = this.$('.o_mail_compose_message_input');
|
||||
this.$input.focus();
|
||||
|
||||
// attachments
|
||||
$(window).on(this.fileupload_id, this.on_attachment_loaded);
|
||||
this.on("change:attachment_ids", this, this.attachment_render);
|
||||
|
||||
// emoji
|
||||
self.$('.o_mail_compose_message_button_emoji').popover({
|
||||
placement: 'top',
|
||||
content: function(){
|
||||
if(!self.$emoji){ // lazy rendering
|
||||
self.$emoji = $(QWeb.render('mail.ComposeMessage.emoji', {'widget': self}));
|
||||
self.$emoji.filter('.o_mail_compose_message_emoji_img').on('click', self, self.on_click_emoji_img);
|
||||
}
|
||||
return self.$emoji;
|
||||
},
|
||||
html: true,
|
||||
container: '.o_mail_compose_message_emoji',
|
||||
trigger: 'focus',
|
||||
});
|
||||
|
||||
// mention
|
||||
this.on('change:mention_word', this, this.mention_word_change);
|
||||
this.on('change:mention_partners', this, this.mention_render_partner);
|
||||
this.on('change:mention_selected_partners', this, this.mention_render_selected_partners);
|
||||
return this._super();
|
||||
},
|
||||
// events
|
||||
on_click_attachment: function(event){
|
||||
event.preventDefault();
|
||||
this.$('input.o_form_input_file').click();
|
||||
},
|
||||
on_click_emoji_img: function(event){
|
||||
this.$input.val(this.$input.val() + " " + $(event.currentTarget).data('emoji')+ " ");
|
||||
this.$input.focus();
|
||||
},
|
||||
on_keydown: function(event){
|
||||
// Save the old input
|
||||
this.input_buffer = this.$input.val();
|
||||
// Key Down displatching
|
||||
switch(event.which) {
|
||||
// BACKSPACE : check if need to remove a mention
|
||||
case $.ui.keyCode.BACKSPACE:
|
||||
this.mention_check_remove(event.which);
|
||||
break;
|
||||
// ENTER : submit the message only if the dropdown mention proposition is not displayed
|
||||
case $.ui.keyCode.ENTER:
|
||||
if(!this.get('mention_partners').length && this.options.display_mode === 'chat'){
|
||||
this.message_send();
|
||||
}
|
||||
break;
|
||||
// UP, DOWN : when navigate on the dropdown, prevent moving the cursor position in the input text
|
||||
case $.ui.keyCode.UP:
|
||||
case $.ui.keyCode.DOWN:
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
},
|
||||
message_send: function(){
|
||||
var mes = mail_utils.get_text2html(this.$input.val());
|
||||
if (! mes.trim() && this.do_check_attachment_upload()) {
|
||||
return;
|
||||
}
|
||||
this.$input.val("");
|
||||
this.message_post(mes, _.pluck(this.get('attachment_ids'), 'id'));
|
||||
},
|
||||
on_keypress: function(event){
|
||||
switch(event.which) {
|
||||
// ENTER : prevent moving cursor position when selecting a mention proposition in dropdown
|
||||
case $.ui.keyCode.ENTER:
|
||||
if(this.get('mention_partners').length){
|
||||
event.preventDefault();
|
||||
}
|
||||
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:
|
||||
case $.ui.keyCode.ESCAPE:
|
||||
// do nothing
|
||||
break;
|
||||
// ENTER, UP, DOWN : check if navigation in mention propositions
|
||||
case $.ui.keyCode.UP:
|
||||
case $.ui.keyCode.DOWN:
|
||||
case $.ui.keyCode.ENTER:
|
||||
this.mention_proposition_navigation(event.which);
|
||||
break;
|
||||
// DELETE : check if need to remove a mention
|
||||
case $.ui.keyCode.DELETE:
|
||||
this.mention_check_remove(event.which);
|
||||
break;
|
||||
// Otherwise, check is a mention is typed
|
||||
default:
|
||||
var mention_word = this.mention_detect_delimiter();
|
||||
if(mention_word){
|
||||
this.set('mention_word', mention_word);
|
||||
}else{
|
||||
this.set('mention_partners', []); // close the dropdown
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Sent the message to the server to create a mail.message, using 'message_post' method
|
||||
* @param {String} body : the core of the message (html, or plaintext)
|
||||
* @param {Number[]} attachment_ids : attachment_ids linked to the message
|
||||
* @param {Object} kwargs : other parameters (in a dictionnary)
|
||||
* @returns {Deferred} resolved when the message is posted, and returning the new message id
|
||||
*/
|
||||
message_post: function(body, attachment_ids, kwargs){
|
||||
var self = this;
|
||||
var values = _.defaults(kwargs || {}, {
|
||||
'message_type': 'comment',
|
||||
'content_subtype': 'html',
|
||||
'partner_ids': [],
|
||||
'subtype': 'mail.mt_comment',
|
||||
});
|
||||
values = _.extend(values, {
|
||||
'body': this.mention_preprocess_message(body),
|
||||
'attachment_ids': attachment_ids || [],
|
||||
'partner_ids': _.pluck(this.get('mention_selected_partners'), 'id').concat(values.partner_ids),
|
||||
});
|
||||
return this.thread_dataset._model.call('message_post', [this.context.default_res_id], values).then(function(message_id){
|
||||
self.reset(); // empty attachment, mention partners, ...
|
||||
self.trigger('message_sent', message_id);
|
||||
return message_id;
|
||||
});
|
||||
},
|
||||
// attachment business
|
||||
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);
|
||||
|
||||
// TODO JEM : understand the 2 lines below ....
|
||||
var $input = this.$('input.o_form_input_file');
|
||||
$input.after($input.clone(true)).remove();
|
||||
|
||||
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;
|
||||
},
|
||||
attachment_render: function(){
|
||||
this.$attachments_list.html(QWeb.render('mail.ComposeMessage.attachments', {'widget': this}));
|
||||
|
||||
// display image thumbnail
|
||||
this.$attachments_list.find(".o_image[data-mimetype^='image']").each(function () {
|
||||
var $img = $(this);
|
||||
if (/gif|jpe|jpg|png/.test($img.data('mimetype')) && $img.data('src')) {
|
||||
$img.css('background-image', "url('" + $img.data('src') + "')");
|
||||
}
|
||||
});
|
||||
},
|
||||
// Mention
|
||||
on_click_mention_item: function(event){
|
||||
event.preventDefault();
|
||||
|
||||
var text_input = this.$input.val();
|
||||
var partner_id = this.$(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 partner
|
||||
// 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_cursor_position());
|
||||
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 mention_word = this.get('mention_word');
|
||||
var cursor_position = this.get_cursor_position();
|
||||
var text_left = text_input.substring(0, cursor_position-(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 = this.$(event.currentTarget);
|
||||
this.$('.o_mail_mention_proposition').removeClass('active');
|
||||
$elem.addClass('active');
|
||||
},
|
||||
mention_proposition_navigation: function(keycode){
|
||||
var $active = this.$('.o_mail_mention_proposition.active');
|
||||
if(keycode === $.ui.keyCode.ENTER){ // selecting proposition
|
||||
if($active){
|
||||
$active.click();
|
||||
}
|
||||
}else{ // navigation in propositions
|
||||
var $to;
|
||||
if(keycode === $.ui.keyCode.DOWN){
|
||||
$to = $active.next('.o_mail_mention_proposition:not(.active)');
|
||||
}else{
|
||||
$to = $active.prev('.o_mail_mention_proposition:not(.active)');
|
||||
}
|
||||
if($to.length){
|
||||
this.$('.o_mail_mention_proposition').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 self = this;
|
||||
var delimiter = self.options.mention_delimiter;
|
||||
var validate_keyword = function(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 > self.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;
|
||||
};
|
||||
var text_val = this.$input.val();
|
||||
var left_string = text_val.substring(0, this.get_cursor_position());
|
||||
var search_str = text_val.substring(left_string.lastIndexOf(delimiter) - 1, this.get_cursor_position());
|
||||
return validate_keyword(search_str);
|
||||
},
|
||||
mention_word_change: function(){
|
||||
var self = this;
|
||||
var word = this.get('mention_word');
|
||||
if(word){
|
||||
// start a timeout to fetch partner with the current 'mention word'. The timer avoid to start
|
||||
// a 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();
|
||||
}, this.options.mention_typing_speed);
|
||||
}else{
|
||||
// when deleting mention word, avoid RPC call with last long enough word
|
||||
clearTimeout(this.mention_fetch_timer);
|
||||
}
|
||||
},
|
||||
mention_fetch_partner: function(){
|
||||
var self = this;
|
||||
var search_str = this.get('mention_word');
|
||||
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(keycode){
|
||||
var mention_selected_partners = this.get('mention_selected_partners');
|
||||
var cursor = this.get_cursor_position();
|
||||
var input_text = this.$input.val();
|
||||
var matches;
|
||||
switch(keycode) {
|
||||
// Remove a mention when a character belonging to a mention word is removed from the input text
|
||||
case $.ui.keyCode.BACKSPACE:
|
||||
matches = this.mention_get_match(this.$input.val());
|
||||
for(var i=0 ; i< matches.length ; i++){
|
||||
var m = matches[i];
|
||||
if(m.index <= cursor && cursor <= m.index+m[0].length){
|
||||
mention_selected_partners.splice(i, 1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Remove a mention if the deleted string overlapse a mention
|
||||
case $.ui.keyCode.DELETE:
|
||||
var left_text = input_text.substring(0, cursor);
|
||||
var right_text = input_text.substring(cursor, input_text.length);
|
||||
var temp = this.input_buffer.substring(0, this.input_buffer.length - right_text.length);
|
||||
var deleted_text = temp.substring(left_text.length, temp.length);
|
||||
|
||||
var deleted_binf = left_text.length;
|
||||
var deleted_bsup = left_text.length + deleted_text.length;
|
||||
|
||||
matches = this.mention_get_match(this.input_buffer);
|
||||
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){
|
||||
mention_selected_partners.splice(i, 1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
this.set('mention_selected_partners', _.clone(mention_selected_partners));
|
||||
},
|
||||
mention_preprocess_message: function(body){
|
||||
var partners = this.get('mention_selected_partners');
|
||||
if(partners.length){
|
||||
var matches = this.mention_get_match(body);
|
||||
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 subtext = body.substring(start_index, end_index);
|
||||
subtext = subtext.replace(match[0], _.str.sprintf("<span data-oe-model='res.partner' data-oe-id='%s'>%s</span>", partners[i].id, match[0]));
|
||||
substrings.push(subtext);
|
||||
start_index = end_index;
|
||||
}
|
||||
substrings.push(body.substring(start_index, body.length));
|
||||
return substrings.join('');
|
||||
}
|
||||
return body;
|
||||
},
|
||||
mention_render_partner: function(){
|
||||
this.$mention_dropdown.html(QWeb.render('mail.ComposeMessage.mention_menu', {'widget': this}));
|
||||
if(this.get('mention_partners').length){
|
||||
this.$mention_dropdown.addClass('open');
|
||||
}else{
|
||||
this.$mention_dropdown.removeClass('open');
|
||||
}
|
||||
},
|
||||
mention_render_selected_partners: function(){
|
||||
this.$mention_partner_tags.html(QWeb.render('mail.ComposeMessage.mention_tags', {'widget': this}));
|
||||
},
|
||||
/**
|
||||
* Return the matches (as RexExp.exec do) 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;
|
||||
}else{
|
||||
if(i === matches.length-1){
|
||||
return i+1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
// others
|
||||
reset: function(){
|
||||
this.set('attachment_ids', []);
|
||||
this.set('mention_partners', []);
|
||||
this.set('mention_selected_partners', []);
|
||||
},
|
||||
focus: function(){
|
||||
this.$input.focus();
|
||||
},
|
||||
get_cursor_position: function() {
|
||||
var el = this.$input.get(0);
|
||||
if(!el){
|
||||
return 0;
|
||||
}
|
||||
if('selectionStart' in el) {
|
||||
return el.selectionStart;
|
||||
} else if('selection' in document) {
|
||||
var cr = document.selection.createRange();
|
||||
return cr.moveStart('character', -el.focus().value.length).text.length - cr.text.length;
|
||||
}
|
||||
return 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();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Mail Thread Mixin : Messages Managment
|
||||
*
|
||||
* Load, Fetch, Display mail.message
|
||||
* This is a mixin since it will be inherit by a form_common.AbstractField and a Wigdet (Client Action)
|
||||
*
|
||||
* @mixin
|
||||
**/
|
||||
var MailThreadMixin = {
|
||||
/**
|
||||
* Constructor of the 'mail_thread' mixin
|
||||
* @param {Boolean} [options.display_log_button] : display of the 'log a note' button
|
||||
* @param {Boolean} [options.display_document_link] : display document link on message
|
||||
* @param {Boolean} [options.display_needaction_button] : display the needaction 'check' button on messages
|
||||
* @param {Object[]} [options.internal_subtypes]: list of internal subtype (for employee only)
|
||||
* @param {Object[]} [options.emoji_list] : list of emoji
|
||||
*/
|
||||
init: function(){
|
||||
this.MessageDatasetSearch = new data.DataSetSearch(this, 'mail.message');
|
||||
this.set('messages', []);
|
||||
this.emoji_substitution = {};
|
||||
// options
|
||||
this.options = _.defaults(this.options || {}, {
|
||||
'display_log_button': false,
|
||||
'display_document_link': false,
|
||||
'display_needaction_button': false,
|
||||
'internal_subtypes': [],
|
||||
'emoji_list': [],
|
||||
'default_username': _t('Anonymous'),
|
||||
});
|
||||
},
|
||||
start: function(){
|
||||
this.on("change:messages", this, this.message_render);
|
||||
},
|
||||
// Common Actions (They should be bind on the implementing widget, the 'events' dict)
|
||||
/**
|
||||
* Generic redirect action : redirect to the form view if the
|
||||
* click node contains 'data-oe-model' and 'data-oe-id'.
|
||||
*/
|
||||
on_click_redirect: function(event){
|
||||
event.preventDefault();
|
||||
var res_id = $(event.target).data('oe-id');
|
||||
var res_model = $(event.target).data('oe-model');
|
||||
web_client.action_manager.do_push_state({
|
||||
'model': res_model,
|
||||
'id': res_id,
|
||||
'title': this.record_name,
|
||||
});
|
||||
this.do_action({
|
||||
type:'ir.actions.act_window',
|
||||
view_type: 'form',
|
||||
view_mode: 'form',
|
||||
res_model: res_model,
|
||||
views: [[false, 'form']],
|
||||
res_id: res_id,
|
||||
}, {
|
||||
'on_reverse_breadcrumb': this.on_reverse_breadcrumb,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Will be called when clicking on the Breadcrumb (mainly of client action)
|
||||
* Need to be override
|
||||
*/
|
||||
on_reverse_breadcrumb: function(){
|
||||
|
||||
},
|
||||
/**
|
||||
* Toggle short / long body when present (hide signature / crop long
|
||||
messages)
|
||||
*/
|
||||
on_message_body_toggle: function (event) {
|
||||
event.preventDefault();
|
||||
var $source = this.$(event.currentTarget);
|
||||
$source.parents('.o_mail_thread_message_core').find('.o_mail_body_short').toggle();
|
||||
$source.parents('.o_mail_thread_message_core').find('.o_mail_body_long').toggle();
|
||||
},
|
||||
/**
|
||||
* Toggle a message as 'starred' (or not), by toggling the 'o_mail_message_starred' css class.
|
||||
* Trigger when clicking on selector '.o_mail_thread_message_star'.
|
||||
*/
|
||||
on_message_star: function(event){
|
||||
var self = this;
|
||||
var $source = this.$(event.currentTarget);
|
||||
var mid = $source.data('message-id');
|
||||
var is_starred = !$source.hasClass('o_mail_message_starred');
|
||||
return this.MessageDatasetSearch.call('set_message_starred', [[mid], is_starred]).then(function(){
|
||||
$source.toggleClass('fa-star-o fa-star o_mail_message_starred');
|
||||
self._message_star([mid], $source.hasClass('o_mail_message_starred'));
|
||||
return mid;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Star a formatted message in local
|
||||
* @param Int[] message_ids : the identifiers of the message to set as (none-)starred by current user
|
||||
* @param Boolean star : star or not the given messages
|
||||
*/
|
||||
_message_star: function(message_ids, star){
|
||||
_.each(this.get('messages'), function(m){
|
||||
if(_.contains(message_ids, m.id)){
|
||||
if(star){
|
||||
m.is_starred = true;
|
||||
m.starred_partner_ids.push(session.partner_id);
|
||||
}else{
|
||||
m.is_starred = false;
|
||||
m.starred_partner_ids = _.without(m.starred_partner_ids, session.partner_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
on_message_needaction: function(event){
|
||||
var self = this;
|
||||
var $source = this.$(event.currentTarget);
|
||||
var mid = $source.data('message-id');
|
||||
return this.MessageDatasetSearch.call('set_message_done', [[mid]]).then(function(){
|
||||
$source.remove();
|
||||
self._message_needaction([mid]);
|
||||
return mid;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Make formatted messages a needaction
|
||||
* @param Int[] message_ids : message ids to mark as needaction treated
|
||||
*/
|
||||
_message_needaction: function(message_ids, needaction){
|
||||
_.each(this.get('messages'), function(m){
|
||||
if(_.contains(message_ids, m.id)){
|
||||
m.is_needaction = false;
|
||||
m.needaction_partner_ids = _.without(m.starred_partner_ids, session.partner_id);
|
||||
}
|
||||
});
|
||||
},
|
||||
// Message functions
|
||||
/**
|
||||
* Fetch given message
|
||||
* @param {Number[]} message_ids : list of mail.message identifiers to fetch
|
||||
* @returns {Deferred} resolved when the messages are loaded
|
||||
*/
|
||||
message_format: function(message_ids){
|
||||
return this.MessageDatasetSearch._model.call('message_format', [message_ids]);
|
||||
},
|
||||
/**
|
||||
* Fetch mail.message in the format defined server side
|
||||
* @param {Array} domain : Odoo Domain of the message to fetch
|
||||
* @param {Number} limit : the limit of messages to fetch
|
||||
* @returns {Deferred} resolved when the messages are loaded
|
||||
*/
|
||||
message_fetch: function(domain, limit){
|
||||
domain = domain || this.get_message_domain();
|
||||
limit = limit || LIMIT_MESSAGE;
|
||||
return this.MessageDatasetSearch._model.call('message_fetch', [domain], {limit: limit});
|
||||
},
|
||||
_message_replace: function(messages){
|
||||
this.set('messages', this._message_order(messages));
|
||||
},
|
||||
/**
|
||||
* Order the message in a particular order, to be defined by implementation
|
||||
* @param {Object[]} messages : messages to order
|
||||
*/
|
||||
_message_order: function(messages){
|
||||
return _.sortBy(messages, 'date');
|
||||
},
|
||||
/**
|
||||
* Insert messages in the current list
|
||||
* @param {Object[]} messages : list of mail.message (formatted server side)
|
||||
*/
|
||||
message_insert: function(raw_messages){
|
||||
var current_messages = this.get('messages');
|
||||
current_messages = current_messages.concat(this._message_preprocess(raw_messages));
|
||||
this._message_replace(current_messages);
|
||||
},
|
||||
/**
|
||||
* Load history of message, with the 'history' domain, and insert them in the current list
|
||||
* @returns {Deferred} resolved when the messages are fetched and returning the raw
|
||||
* messages (formatted server side)
|
||||
*/
|
||||
message_load_history: function(){
|
||||
var self = this;
|
||||
return this.message_fetch(this.get_message_domain_history()).then(function(raw_messages){
|
||||
if (raw_messages.length < LIMIT_MESSAGE) {
|
||||
self.all_messages_loaded = true;
|
||||
}
|
||||
self.message_insert(raw_messages);
|
||||
return raw_messages;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Load messages according to the current domain, and set that new list as current messages,
|
||||
* erasing the old ones.
|
||||
* @returns {Deferred} resolved when the messages are fetched and returning the raw
|
||||
* messages (formatted server side)
|
||||
*/
|
||||
message_load_new: function(){
|
||||
var self = this;
|
||||
return this.message_fetch(this.get_message_domain()).then(function(raw_messages){
|
||||
self.all_messages_loaded = raw_messages.length < LIMIT_MESSAGE;
|
||||
self._message_replace(self._message_preprocess(raw_messages));
|
||||
return raw_messages;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Preprocess the list of messages before rendering, add 'computed' field (is_needaction,
|
||||
* is_starred, ...), and apply image shortcode to the message body.
|
||||
* @param {Object[]} messages : list of raw mail.message (formatted server side)
|
||||
* @returns list of messages. It can be sorted, grouped, ...
|
||||
*/
|
||||
_message_preprocess: function(messages){
|
||||
var self = this;
|
||||
_.each(messages, function(m){
|
||||
m.is_needaction = _.contains(m.needaction_partner_ids, session.partner_id);
|
||||
m.is_starred = _.contains(m.starred_partner_ids, session.partner_id);
|
||||
m.date = moment(time.str_to_datetime(m.date)).format('YYYY-MM-DD HH:mm:ss'); // set the date in the correct browser user timezone
|
||||
if(m.body){
|
||||
m.body = mail_utils.shortcode_apply(m.body, self.emoji_substitution);
|
||||
m.body = self._message_preprocess_mention(m.body);
|
||||
}
|
||||
_.each(m.attachment_ids, function(a){
|
||||
a.url = '/web/content/' + a.id + '?download=true';
|
||||
});
|
||||
});
|
||||
return _.sortBy(messages, 'date');
|
||||
},
|
||||
/**
|
||||
* Transform the mention 'span' tag into a link tag to the partner form
|
||||
* @param {String} body : the message body (html) to transform
|
||||
* @returns {String} the transformed body
|
||||
*/
|
||||
_message_preprocess_mention: function(body){
|
||||
var re = /<span data-oe-model="([^"]*)"\s+data-oe-id="([^"]*)">(.*?)\<\/span>/g;
|
||||
var subst = "<a href='#model=$1&id=$2' class='o_mail_redirect' data-oe-model='$1' data-oe-id='$2'>$3</a>";
|
||||
return body.replace(re, subst);
|
||||
},
|
||||
/**
|
||||
* Take the current messages, render them, and insert the rendering in the DOM.
|
||||
* This is triggered when the mesasge list change.
|
||||
* Must be redefined, since it depends on the complete DOM widget
|
||||
*/
|
||||
message_render: function(){
|
||||
|
||||
},
|
||||
// Message Domains
|
||||
/**
|
||||
* Return the current domain to fetch the message of the thread. this should
|
||||
* be redifined by the thread implementation.
|
||||
*/
|
||||
get_message_domain: function(){
|
||||
return [];
|
||||
},
|
||||
get_message_domain_history: function(){
|
||||
return this.get_message_domain().concat([['id', '<', _.min(_.pluck(this.get('messages'), 'id'))]]);
|
||||
},
|
||||
// Others
|
||||
/**
|
||||
* Set the list of emoji to be substituted in message body
|
||||
* @param {Object[]} emoji_list : list of emoji Object
|
||||
*/
|
||||
emoji_set_substitution: function(emoji_list){
|
||||
this.emoji_substitution = mail_utils.shortcode_substitution(emoji_list);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
MailComposeMessage: MailComposeMessage,
|
||||
MailThreadMixin: MailThreadMixin,
|
||||
LIMIT_MESSAGE: LIMIT_MESSAGE,
|
||||
};
|
||||
|
||||
|
||||
});
|
||||
@@ -1,134 +0,0 @@
|
||||
odoo.define('mail.utils', function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* ------------------------------------------------------------
|
||||
* ChatterUtils
|
||||
* ------------------------------------------------------------
|
||||
*
|
||||
* This class holds a few tools method for Chatter.
|
||||
* Some regular expressions not used anymore, kept because I want to
|
||||
* - (^|\s)@((\w|@|\.)*): @login@log.log
|
||||
* - (^|\s)\[(\w+).(\w+),(\d)\|*((\w|[@ .,])*)\]: [ir.attachment,3|My Label],
|
||||
* for internal links
|
||||
*/
|
||||
|
||||
|
||||
/** parse text to find email: Tagada <address@mail.fr> -> [Tagada, address@mail.fr] or False */
|
||||
function parse_email(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 ?
|
||||
*/
|
||||
function get_text2html(text) {
|
||||
return text
|
||||
.replace(/((?:https?|ftp):\/\/[\S]+)/g,'<a href="$1">$1</a> ')
|
||||
.replace(/[\n\r]/g,'<br/>');
|
||||
}
|
||||
|
||||
/**
|
||||
* ------------------------------------------------------------
|
||||
* MailChat Utils
|
||||
* ------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Apply the given shortcode to the 'str' message.
|
||||
* @param str : the text to substitute (html).
|
||||
* @param shortcodes : dict where key is the shortcode, and the value is the substitution
|
||||
*/
|
||||
function shortcode_apply(str, shortcodes){
|
||||
var re_escape = function(str){
|
||||
return String(str).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1');
|
||||
};
|
||||
_.each(_.keys(shortcodes), function(key){
|
||||
str = str.replace( new RegExp("(?:^|\\s|<[a-z]*>)(" + re_escape(key) + ")(?:\\s|$|</[a-z]*>)"), ' <span class="o_mail_emoji">'+shortcodes[key]+'</span> ');
|
||||
});
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the list of emoji (shortcode Object) into a key-array representing
|
||||
* the substitution to apply
|
||||
* @param {Ojbect[]} emoji_list : list of emoji object
|
||||
* @returns {Object} mapping between the code (as key) and the substitution (as value)
|
||||
*/
|
||||
function shortcode_substitution(emoji_list){
|
||||
var emoji_substitution = {};
|
||||
_.each(emoji_list, function(emoji){
|
||||
emoji_substitution[emoji.source] = emoji.substitution;
|
||||
});
|
||||
return emoji_substitution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate a font awesome class to a attachment file type
|
||||
* @param {string} mimetype : mimetype to analyse
|
||||
* @returns {string} the fa class associated
|
||||
*/
|
||||
function attachment_mimetype_icon(mimetype){
|
||||
var mapping = {
|
||||
'text/plain': 'fa fa-file-text-o',
|
||||
'text/css': 'fa fa-file-code-o',
|
||||
'text/javascript': 'fa fa-file-code-o',
|
||||
'text/html': 'fa fa-file-code-o',
|
||||
'image/jpeg': 'fa fa-file-photo-o',
|
||||
'image/png': 'fa fa-file-photo-o',
|
||||
'image/gif': 'fa fa-file-photo-o',
|
||||
'application/octet-stream': 'fa fa-file-code-o',
|
||||
'application/pdf': 'fa fa-file-pdf-o',
|
||||
'application/zip': 'fa fa-file-archive-o',
|
||||
'application/x-compressed': 'fa fa-file-archive-o',
|
||||
'application/msword': 'fa fa-file-word-o',
|
||||
'application/vnd.ms-excel': 'fa fa-file-excel-o',
|
||||
'application/mspowerpoint': 'fa fa-file-powerpoint-o',
|
||||
'application/powerpoint': 'fa fa-file-powerpoint-o',
|
||||
'application/vnd.ms-powerpoint': 'fa fa-file-powerpoint-o',
|
||||
'audio/mpeg3': 'fa fa-file-audio-o',
|
||||
'audio/x-mpeg-3': 'fa fa-file-audio-o',
|
||||
'audio/mpeg': 'fa fa-file-audio-o',
|
||||
'audio/midi': 'fa fa-file-audio-o',
|
||||
'video/msvideo': 'fa fa-file-movie-o',
|
||||
'video/mpeg': 'fa fa-file-movie-o',
|
||||
'video/quicktime': 'fa fa-file-movie-o',
|
||||
};
|
||||
if(_.contains(_.keys(mapping), mimetype)){
|
||||
return mapping[mimetype];
|
||||
}
|
||||
return 'fa fa-file-o';
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the noise 'ting' to notify user
|
||||
*/
|
||||
function beep(session){
|
||||
if (typeof(Audio) === "undefined") {
|
||||
return;
|
||||
}
|
||||
var audio = new Audio();
|
||||
var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3";
|
||||
audio.src = session.url("/mail/static/src/audio/ting") + ext;
|
||||
audio.play();
|
||||
}
|
||||
|
||||
return {
|
||||
parse_email: parse_email,
|
||||
get_text2html: get_text2html,
|
||||
shortcode_apply: shortcode_apply,
|
||||
shortcode_substitution: shortcode_substitution,
|
||||
attachment_mimetype_icon: attachment_mimetype_icon,
|
||||
beep: beep,
|
||||
};
|
||||
|
||||
});
|
||||
@@ -1,128 +0,0 @@
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
.o_mail_chat {
|
||||
.o-position-absolute(0, 0, 0, 0);
|
||||
height: 100%;
|
||||
|
||||
@media (max-width: @screen-xs-max) {
|
||||
position: static;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.o-flex-display();
|
||||
.o-flex-flow(row, nowrap);
|
||||
|
||||
.o_mail_chat_sidebar {
|
||||
.o-flex(0, 0, 15%);
|
||||
overflow: auto;
|
||||
padding: 1%;
|
||||
}
|
||||
|
||||
.o_mail_chat_content {
|
||||
.o-flex(0, 0, 85%);
|
||||
height: 100%;
|
||||
.o-flex-display();
|
||||
.o-flex-flow(column, nowrap);
|
||||
@media (max-width: @screen-xs-max) {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.o_mail_chat_messages {
|
||||
.o-flex(1, 0, 0);
|
||||
overflow: auto;
|
||||
.o-webclient-padding();
|
||||
|
||||
.o_mail_chat_help_message {
|
||||
padding: 25px 0;
|
||||
text-align: justify;
|
||||
}
|
||||
}
|
||||
|
||||
.o_mail_chat_composer_wrapper {
|
||||
.o-flex(0, 0, auto);
|
||||
.o-webclient-padding(@top: 10px, @bottom: 5px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Style
|
||||
// ------------------------------------------------------------------
|
||||
.o_mail_chat {
|
||||
.o_mail_chat_sidebar {
|
||||
background-color: @gray;
|
||||
|
||||
> .nav-pills {
|
||||
> li {
|
||||
> a {
|
||||
color: @odoo-brand-optional; // community compatibility: override base.css
|
||||
&:hover, &:focus {
|
||||
background-color: @gray-dark;
|
||||
}
|
||||
&.active {
|
||||
background-color: @gray-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&.o_mail_chat_channel_item, &.o_mail_chat_partner_item {
|
||||
> a {
|
||||
color: @gray-lighter;
|
||||
> .badge {
|
||||
color: white;
|
||||
background-color: @odoo-brand-optional;
|
||||
}
|
||||
|
||||
> i.fa {
|
||||
padding-top: 3px;
|
||||
padding-right: 3px;
|
||||
|
||||
&.o_online {
|
||||
color: @odoo-brand-optional;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_mail_chat_channel_item.o_mail_chat_channel_unread, .o_mail_chat_partner_item.o_mail_chat_channel_unread{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.o_mail_chat_sidebar_title {
|
||||
text-transform: uppercase;
|
||||
color: @gray-lighter;
|
||||
margin-top: 25px;
|
||||
.user-select(none);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.o_mail_chat_add_more_search{
|
||||
padding: @padding-base-vertical @padding-base-horizontal;
|
||||
.o_mail_chat_add_more_text{
|
||||
color: @gray-lighter;
|
||||
display: inline-block;
|
||||
font-style: italic;
|
||||
cursor: pointer;
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
.o_mail_chat_add_more_text:hover{
|
||||
text-decoration: underline;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.o_mail_chat_content {
|
||||
background-color: white;
|
||||
|
||||
.o_mail_chat_composer_wrapper {
|
||||
border-top: 1px solid @gray-lighter;
|
||||
background-color: #21b799;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
|
||||
/* emoji size */
|
||||
.o_mail_emoji img{
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Chat Window
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@media screen and (max-width:360px) {
|
||||
.o_mail_chat_im_window {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
.o_mail_chat_im_window .o_mail_chat_im_window_content {
|
||||
width: 100% !important;
|
||||
}
|
||||
.o_mail_chat_im_window .o_mail_chat_im_window_input {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.o_mail_chat_im_window {
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
margin-bottom: 5px;
|
||||
margin-right: 6px;
|
||||
background: rgba(250, 250, 250, 0.95);
|
||||
border: 1px solid #DEDEDE;
|
||||
width: 240px;
|
||||
z-index:500;
|
||||
}
|
||||
|
||||
/* HEADER */
|
||||
.o_mail_chat_im_window .o_mail_chat_im_window_header {
|
||||
padding: 3px 6px 2px;
|
||||
cursor: pointer;
|
||||
background: #DEDEDE;
|
||||
background: -moz-linear-gradient(#FCFCFC, #DEDEDE);
|
||||
background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE));
|
||||
border-radius: 3px 3px 0 0;
|
||||
-moz-border-radius: 3px 3px 0 0;
|
||||
-webkit-border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.o_mail_chat_im_window .o_mail_chat_im_window_header_name {
|
||||
max-width: 75%;
|
||||
word-wrap: break-word;
|
||||
display: inline-block;
|
||||
height: 15px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.o_mail_chat_im_window .o_mail_chat_im_window_header_buttons{
|
||||
float: right;
|
||||
}
|
||||
|
||||
|
||||
/* CONTENT (messages, ...) */
|
||||
.o_mail_chat_im_window .o_mail_chat_im_window_content {
|
||||
height: 280px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.o_mail_chat_im_window .o_mail_chat_im_messages .o_mail_chat_im_window_date_sperator{
|
||||
padding: 5px;
|
||||
padding-right: 10px;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.o_mail_chat_im_window .o_mail_chat_im_message{
|
||||
margin: auto;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.o_mail_chat_im_window .o_mail_chat_im_message .o_mail_chat_im_message_avatar{
|
||||
width: 20%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.o_mail_chat_im_window .o_mail_chat_im_message .o_mail_chat_im_message_avatar img{
|
||||
width: 80%;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.o_mail_chat_im_window .o_mail_chat_im_message .o_mail_chat_im_message_core{
|
||||
width: 75%;
|
||||
display: inline-block;
|
||||
padding: 5px;
|
||||
|
||||
background: white;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
max-width:100%;
|
||||
}
|
||||
|
||||
.o_mail_chat_im_window .o_mail_chat_im_message .o_mail_chat_im_message_core p{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* FOOTER */
|
||||
.o_mail_chat_im_window .o_mail_chat_im_window_footer {
|
||||
padding:3px;
|
||||
background: #DEDEDE;
|
||||
}
|
||||
|
||||
.o_mail_chat_im_window .o_mail_chat_im_window_footer .o_mail_chat_im_window_input {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
.o_mail_chatter {
|
||||
width: 100%;
|
||||
|
||||
.o_mail_chatter_compose_message {
|
||||
display: none;
|
||||
margin-top: 10px;
|
||||
.o_mail_chatter_compose_message_suggested_partners label {
|
||||
vertical-align: middle;
|
||||
display: block;
|
||||
line-height: 14px;
|
||||
}
|
||||
.o_mail_compose_message_buttons > button {
|
||||
.btn-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.o_mail_chatter_messages {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
.o_mail_followers {
|
||||
height: 0px; // to be on the same line as mail composer buttons
|
||||
float: right;
|
||||
@media (min-width: @screen-sm-min) {
|
||||
&.o_form_field {
|
||||
margin: 0; // override o_form_field's margin-bottom of 5px
|
||||
}
|
||||
}
|
||||
@media (max-width: @screen-xs-max) {
|
||||
height: auto; // to be displayed above the mail composer buttons
|
||||
float: left;
|
||||
}
|
||||
|
||||
.o_followers_actions {
|
||||
position: relative;
|
||||
float: right;
|
||||
|
||||
.o_followers_follow_button {
|
||||
width: 110px;
|
||||
|
||||
.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: 120px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
// ------------------------------------------------------------------
|
||||
// Mail Thread 'Mixin'
|
||||
// ------------------------------------------------------------------
|
||||
.o_mail_thread {
|
||||
.o_mail_thread_date_separator, .o_mail_thread_message {
|
||||
margin: auto; // override the margin-{left,right}: -15px defined on .row
|
||||
}
|
||||
.o_mail_thread_date_separator {
|
||||
.o_mail_thread_date_wrapper {
|
||||
border-bottom: solid @gray-lighter 2px;
|
||||
margin-bottom: 15px;
|
||||
.o_mail_thread_date_separator_title {
|
||||
display: table;
|
||||
position: relative;
|
||||
top: 10px;
|
||||
margin: 0 auto;
|
||||
padding: 0 10px 0 10px;
|
||||
font-weight: bold;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
@mail_thread_avatar_size: 50px;
|
||||
.o_mail_thread_message {
|
||||
.o-flex-display();
|
||||
margin: 10px 0;
|
||||
.o_mail_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_mail_thread_avatar {
|
||||
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_mail_thread_message_core {
|
||||
.o-flex(1, 1, auto);
|
||||
> p {
|
||||
margin: 0 0 9px; // Required by the old design to override a general rule on p's
|
||||
}
|
||||
.o_mail_body_long {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.o_mail_thread_message_star, .o_mail_thread_message_needaction {
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.o_mail_thread_message_star.o_mail_message_starred {
|
||||
color: gold;
|
||||
}
|
||||
.o_mail_redirect {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.o_mail_thread_show_more {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Mail Compose Message
|
||||
// ------------------------------------------------------------------
|
||||
.o_mail_chat_composer_wrapper {
|
||||
.o_mail_compose_message { // use flex for the layout in the Chat app
|
||||
.o-flex-display();
|
||||
.o_mail_compose_message_input {
|
||||
.o-flex(1, 1, auto);
|
||||
}
|
||||
.o_mail_compose_message_buttons {
|
||||
.o-flex(0, 0, auto);
|
||||
}
|
||||
}
|
||||
}
|
||||
.o_mail_compose_message {
|
||||
width: 100%;
|
||||
input.o_mail_compose_message_input {
|
||||
border: none;
|
||||
padding: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
textarea.o_mail_compose_message_input {
|
||||
width: 100%;
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
.o_mail_mention_partner_tags {
|
||||
margin-top: 5px;
|
||||
text-align: justify;
|
||||
}
|
||||
.o_mail_attachments_list .o_mail_chat_im_attachment_files {
|
||||
margin-top: 5px;
|
||||
text-align: justify;
|
||||
}
|
||||
// Emoji size in the popover
|
||||
.o_mail_compose_message_emoji {
|
||||
.o_mail_compose_message_emoji_img > img {
|
||||
cursor: pointer;
|
||||
height: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Attachment Icons (common for mail thread message and compose message)
|
||||
// ------------------------------------------------------------------
|
||||
@o-mail-attachment-image-size: 60px;
|
||||
|
||||
@-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 {
|
||||
position: relative;
|
||||
width: 2*@o-mail-attachment-image-size;
|
||||
display: inline-block;
|
||||
height: 50px;
|
||||
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_mail_attachment_name {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
margin-top: @o-mail-attachment-image-size;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: @odoo-main-text-color;
|
||||
}
|
||||
|
||||
&.o_mail_uploading {
|
||||
.o_mail_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_mail_attachment_delete {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.o_mail_attachment_progress_bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.o_mail_attachment_delete {
|
||||
.o-position-absolute(@top: 0, @right: 0);
|
||||
color: @odoo-main-text-color;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
.o-transition(opacity, 0.2s, linear);
|
||||
}
|
||||
|
||||
&:hover .o_mail_attachment_delete {
|
||||
opacity: 1;
|
||||
.o-transition(opacity, 0.2s, linear);
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<!-- Notification Counter of the Systray Menu -->
|
||||
<t t-name="mail.chat.NotificationTopButton">
|
||||
<li t-att-title='_t("Notifications")'>
|
||||
<a href="#">
|
||||
<i class="fa fa-comment"><t t-esc="widget.get('counter') || ''"/></i>
|
||||
</a>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
<!-- Add More / Search Box -->
|
||||
<t t-name="mail.chat.AbstractAddMoreSearch">
|
||||
<div class="o_mail_chat_add_more_search">
|
||||
<a href="#" class="o_mail_chat_add_more_text"><t t-esc="widget.label"/></a>
|
||||
<div class="o_mail_chat_add_more_search_bar" style="display: none;">
|
||||
<input type="text" class="o_mail_chat_search_input"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- PartnerInviteDialog : content of the dialog -->
|
||||
<t t-name="mail.chat.PartnerInviteDialog">
|
||||
<div>
|
||||
<input type="text" class="o_mail_chat_partner_invite_input" id="mail_search_partners"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
<!--
|
||||
ChatMailThread
|
||||
Layout of the client action
|
||||
param: widget : the widget (ChatMailThread)
|
||||
-->
|
||||
<t t-name="mail.chat.ChatMailThread">
|
||||
<div class="o_mail_chat o_mail_thread">
|
||||
<!-- Sidebar -->
|
||||
<div class="o_mail_chat_sidebar">
|
||||
<!-- Static channel list -->
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li class="o_mail_chat_channel_item" data-channel-id="channel_inbox">
|
||||
<a href="#">@ Inbox
|
||||
<span class="badge pull-right o_mail_chat_needaction" data-channel-id="channel_inbox" t-att-style="((widget.get('needaction_inbox_counter') > 0) ? 'display:block;' : 'display:none;')">
|
||||
<t t-esc="widget.get('needaction_inbox_counter')"/>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="o_mail_chat_channel_item" data-channel-id="channel_starred">
|
||||
<a href="#"><i class="fa fa-star-o"> Starred</i></a>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- Channel list -->
|
||||
<h4 class="o_mail_chat_sidebar_title"><t t-esc="_t('Channels')"/></h4>
|
||||
<t t-call="mail.chat.ChatMailThread.channels">
|
||||
<t t-set="channel_slot" t-value="'channel_channel'"/>
|
||||
</t>
|
||||
<!-- Direct Message list (res.users)-->
|
||||
<h4 class="o_mail_chat_sidebar_title"><t t-esc="_t('Direct Message')"/></h4>
|
||||
<t t-call="mail.chat.ChatMailThread.partners"/>
|
||||
<!-- Private Group list -->
|
||||
<h4 class="o_mail_chat_sidebar_title"><t t-esc="_t('Private Group')"/></h4>
|
||||
<t t-call="mail.chat.ChatMailThread.channels">
|
||||
<t t-set="channel_slot" t-value="'channel_private_group'"/>
|
||||
</t>
|
||||
</div>
|
||||
<!-- Content -->
|
||||
<div class="o_mail_chat_content">
|
||||
<div class="o_mail_chat_messages"/>
|
||||
<div class="o_mail_chat_composer_wrapper"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
List of channel in the sidebar
|
||||
param: channel_slot : the name of the channel slot
|
||||
param: widget : the widget (ChatMailThread)
|
||||
-->
|
||||
<t t-name="mail.chat.ChatMailThread.channels">
|
||||
<ul t-att-class="'nav nav-pills nav-stacked o_mail_chat_channel_slot_' + channel_slot" data-toggle="pill">
|
||||
<t t-foreach="widget.get(channel_slot)" t-as="channel">
|
||||
<li t-att-class="'o_mail_chat_channel_item ' + (channel.message_needaction_counter > 0 ? 'o_mail_chat_channel_unread' : ' ')" t-att-data-channel-id="channel.id" role="presentation">
|
||||
<a t-att-href="_.str.sprintf('#active_id=%s&action=%s', channel.id, widget.action.id)">
|
||||
<t t-esc="channel.name"/>
|
||||
<span class="badge pull-right o_mail_chat_needaction" t-att-data-channel-id="channel.id" t-att-style="((channel.message_needaction_counter > 0) ? 'display:block;' : 'display:none;')">
|
||||
<t t-esc="channel.message_needaction_counter"/>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
List of Partners of Direct Message channel
|
||||
param: widget : the widget (ChatMailThread)
|
||||
-->
|
||||
<t t-name="mail.chat.ChatMailThread.partners">
|
||||
<ul class="nav nav-pills nav-stacked o_mail_chat_channel_slot_partners">
|
||||
<t t-foreach="widget.get('partners')" t-as="partner">
|
||||
<li class="o_mail_chat_partner_item" t-att-data-partner-id="partner.id" role="presentation">
|
||||
<a href="#">
|
||||
<i t-att-class="'fa ' + (partner.im_status == 'online' ? 'fa-circle' : 'fa-circle-o')"/>
|
||||
<t t-esc="partner.name"/>
|
||||
<span class="fa fa-times pull-right o_mail_chat_partner_unpin" t-att-title="_t('Close')" t-att-data-partner-id="partner.id"/>
|
||||
</a>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
Channel content (messages) or 'Help Message' of the client action
|
||||
param: widget : the widget (ChatMailThread)
|
||||
-->
|
||||
<t t-name="mail.chat.ChatMailThread.content">
|
||||
<t t-if="widget.get('messages').length">
|
||||
<t t-call="mail.Thread.load_more"/>
|
||||
<t t-call="mail.Thread.messages"/>
|
||||
</t>
|
||||
<t t-if="!widget.get('messages').length">
|
||||
<div class="o_mail_chat_help_message">
|
||||
<t t-raw="widget.help_message"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
Buttons of the Control Panel
|
||||
-->
|
||||
<t t-name="mail.chat.ControlButtons">
|
||||
<div>
|
||||
<button type="button" class="btn btn-primary btn-sm o_mail_chat_button_invite" t-att-title='_t("Invite people")'>Invite</button>
|
||||
<button type="button" class="btn btn-default btn-sm o_mail_chat_button_minimize" t-att-title='_t("Minimize the conversation")'>Minimize</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,93 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.chat.im.Conversation">
|
||||
<div class="o_mail_chat_im_window openerp">
|
||||
<div class="o_mail_chat_im_window_header">
|
||||
<span class="o_mail_chat_im_window_header_name">
|
||||
<t t-raw="widget.get('session').name"/>
|
||||
</span>
|
||||
<span class="nbr_messages o_mail_chat_im_window_header_counter"/>
|
||||
<span class="o_mail_chat_im_window_header_buttons">
|
||||
<div class="o_mail_chat_im_window_close">×</div>
|
||||
</span>
|
||||
</div>
|
||||
<div class="o_mail_chat_im_window_content">
|
||||
<t t-call="mail.chat.im.Conversation.messages"/>
|
||||
</div>
|
||||
<div class="oe_im_chatview_footer o_mail_chat_im_window_footer">
|
||||
<input class="o_mail_chat_im_window_input" t-att-placeholder="widget.options.placeholder"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.chat.im.Conversation.messages">
|
||||
<div class="o_mail_chat_im_messages">
|
||||
<t t-set="current_day" t-value="0"/>
|
||||
<t t-foreach="widget.get('messages')" t-as="message">
|
||||
<div t-if="current_day != message.date.split(' ')[0]" class="row o_mail_chat_im_window_date_sperator">
|
||||
<t t-raw="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('LL')"/>
|
||||
<t t-set="current_day" t-value="message.date.split(' ')[0]"/>
|
||||
</div>
|
||||
<div class="o_mail_chat_im_message">
|
||||
<div class="o_mail_chat_im_message_avatar">
|
||||
<img t-if="message.message_type == 'email' && (!message.author_id || !message.author_id[0]) && message.email_from" src="/mail/static/src/img/email_icon.png" alt="avatar" class="img-responsive img-rounded"/>
|
||||
<img t-if="message.message_type == 'email' && (message.author_id && message.author_id[0]) && !message.email_from" t-att-src="'/web/image/res.partner/'+message.author_id[0]+'/image_small'" class="img-responsive img-rounded"/>
|
||||
<img t-if="message.message_type !== 'email' && (message.author_id && message.author_id[0])" t-att-src="'/web/image/mail.message/'+message.id+'/author_avatar'" class="img-responsive img-rounded"/>
|
||||
<img t-if="message.message_type !== 'email' && (!message.author_id || !message.author_id[0])" src="/mail/static/src/img/smiley/avatar.jpg" class="img-responsive img-rounded"/>
|
||||
</div>
|
||||
<div class="o_mail_chat_im_message_core">
|
||||
<!-- author -->
|
||||
<span class="text-muted">
|
||||
<t t-if="message.author_id && message.author_id[0]">
|
||||
<strong><t t-esc="message.author_id[1]"/></strong> -
|
||||
</t>
|
||||
<t t-if="!message.author_id || !message.author_id[0]">
|
||||
<strong><t t-esc="default_username || widget.options.default_username"/></strong> -
|
||||
</t>
|
||||
<t t-if="!message.author_id">
|
||||
<strong><t t-esc="message.email_from"/></strong> -
|
||||
</t>
|
||||
<span class="small"><t t-esc="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('hh:mm')"/></span>
|
||||
</span>
|
||||
<!-- body -->
|
||||
<p class="o_mail_chat_im_message_body">
|
||||
<t t-if="message.body">
|
||||
<t t-raw="message.body"/> <!-- TODO JEM : tracking message and attachments ??!! -->
|
||||
</t>
|
||||
<t t-if="!message.body">
|
||||
<t t-call="mail.chat.im.Conversation.tracking_message"/>
|
||||
</t>
|
||||
</p>
|
||||
<!-- Attachments -->
|
||||
<div t-attf-class="o_mail_chat_im_message_attachment">
|
||||
<t t-foreach="message.attachment_ids" t-as="attachment">
|
||||
<a t-att-href='attachment.url' target="_blank">
|
||||
<i t-att-class="attachment.fa_class" t-att-title="'Download ' + (attachment.name || attachment.filename)"/>
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.chat.im.Conversation.tracking_message">
|
||||
<div>
|
||||
<ul class="o_mail_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 class="o_mail_message_tracking_value"> <t t-esc="value.old_value"/> </span>
|
||||
<span> <i class="fa fa-long-arrow-right"></i> </span>
|
||||
</t>
|
||||
<span class="o_mail_message_tracking_value"> <t t-esc="value.new_value"/> </span>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,98 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
|
||||
<t t-name="mail.chatter.ComposeMessage">
|
||||
<div class="mail_chatter_message_composer">
|
||||
<!-- Log a Note (subtypes)-->
|
||||
<t t-if="widget.options.is_log">
|
||||
<span class="text-muted">
|
||||
Log an internal note which will not be sent to followers.<br/>
|
||||
Note that users reading the document can read internal notes.
|
||||
</span>
|
||||
<!-- Internal subtypes -->
|
||||
<div t-att-class="'form-group col-md-8 ' + (widget.options.internal_subtypes.length <= 1 ? ' hide' : '')">
|
||||
<label class="col-sm-2 control-label">Activity</label>
|
||||
<div class="col-sm-4">
|
||||
<select class="form-control o_mail_chatter_compose_message_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>
|
||||
</div>
|
||||
</t>
|
||||
<!-- New Message (list of followers)-->
|
||||
<t t-if="!widget.options.is_log">
|
||||
<!-- Text for followers -->
|
||||
<span class="text-muted">
|
||||
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_mail_chatter_compose_message_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>
|
||||
<!-- Composer Message -->
|
||||
<t t-call="mail.ComposeMessage"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
<t t-name="mail.chatter.ChatterMailThread">
|
||||
<div class="o_mail_chatter o_mail_thread">
|
||||
<!-- Compose buttons ('new message' and 'log a note') -->
|
||||
<div class="o_mail_chatter_compose_buttons">
|
||||
<button type="button" class="btn btn-sm btn-primary o_mail_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_mail_chatter_button_log_note" title="Log a note. Followers will not be notified.">
|
||||
Log an internal note
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- wrap div for ChatterMailComposeMessage -->
|
||||
<div class="o_mail_chatter_compose_message"/>
|
||||
<!-- Message list -->
|
||||
<div class="o_mail_chatter_messages"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.chatter.ChatterMailThread.messages">
|
||||
<t t-if="widget.get('messages').length">
|
||||
<t t-call="mail.Thread.messages"/>
|
||||
<t t-call="mail.Thread.load_more"/>
|
||||
</t>
|
||||
<t t-if="!widget.get('messages').length">
|
||||
No Messages.
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
ComposeMessage.ButtonTopBar
|
||||
render of the button on the user bar for open wizard compose message
|
||||
-->
|
||||
<t t-name="mail.ComposeMessageTopButton">
|
||||
<li title='Compose new Message' class="oe_topbar_item oe_topbar_compose_full_email">
|
||||
<a href="#">
|
||||
<i class="fa fa-envelope-o fa-fw"></i>
|
||||
</a>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
|
||||
</templates>
|
||||
@@ -1,80 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
|
||||
<!--
|
||||
Mail Follower (root template)
|
||||
Template used to display the followers, the actions and the subtypes in a record.
|
||||
-->
|
||||
<t t-name="mail.Followers">
|
||||
<div class="o_mail_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-primary o_followers_follow_button o_followers_notfollow">
|
||||
<span class="o_follow">Follow</span>
|
||||
<span class="fa fa-times o_followers_unfollow" aria-hidden="true"> Unfollow</span>
|
||||
<span class="fa fa-check o_followers_following" aria-hidden="true"> Following</span>
|
||||
</button><button t-attf-class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="caret"/>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
Mail Followers Partner template
|
||||
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 t-attf-href="#model=res.partner&id=#{record.res_id}"
|
||||
t-att-title="record.name"
|
||||
t-att-data-res-model="record.res_model"
|
||||
t-att-data-res-id="record.res_id"
|
||||
t-att-data-id="record.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-res-model="record.res_model"
|
||||
t-att-data-res-id="record.res_id"/>
|
||||
<i t-if="widget.view_is_editable" class="fa fa-remove o_remove_follower"
|
||||
title="Remove this follower"/>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.Followers.add_more">
|
||||
<li t-if="widget.view_is_editable" class="o_add_follower">
|
||||
<a href="#"> Add followers </a>
|
||||
</li>
|
||||
<li t-if="widget.view_is_editable" class="o_add_follower_channel">
|
||||
<a href="#"> Add Channels </a>
|
||||
</li>
|
||||
<li t-if="widget.view_is_editable and (widget.followers.length > 0)" class="divider"/>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
Mail Followers Subtype template
|
||||
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>
|
||||
@@ -1,196 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
|
||||
<t t-name="mail.ComposeMessage">
|
||||
<div class="o_mail_compose_message">
|
||||
<div class="dropup o_mail_mention_dropdown"/>
|
||||
<!-- composer is an input text if display mode is 'chat' and a textarea if it is 'mail' -->
|
||||
<t t-if="widget.options.display_mode === 'chat'">
|
||||
<input type="text" class="o_mail_compose_message_input" placeholder="Write something" name="message" id="send_message"/>
|
||||
</t>
|
||||
<t t-if="widget.options.display_mode === 'mail'">
|
||||
<textarea class="o_mail_compose_message_input" placeholder="Write something" name="message" id="send_message"/>
|
||||
</t>
|
||||
<div class="btn-group o_mail_compose_message_buttons">
|
||||
<button t-if="widget.options.display_mode === 'mail'" class="btn btn-primary o_mail_compose_message_button_send" type="button">
|
||||
Send
|
||||
</button>
|
||||
<button class="btn btn-icon fa fa-smile-o o_mail_compose_message_button_emoji" type="button" data-toggle="popover"/>
|
||||
<button class="btn btn-icon fa fa-paperclip o_mail_compose_message_button_attachment" type="button" data-toggle="popover"/>
|
||||
<button t-if="widget.options.display_mode === 'mail'" class="btn btn-icon fa fa-pencil-square-o o_mail_compose_message_button_full_message" type="button" data-toggle="popover"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_mail_mention_partner_tags"/>
|
||||
<div class="o_mail_attachments_list"/>
|
||||
|
||||
<span class="o_mail_attachment_file 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_mail_compose_message_emoji"></span>
|
||||
|
||||
</t>
|
||||
|
||||
<t t-name="mail.ComposeMessage.emoji">
|
||||
<t t-foreach="widget.options.emoji_list" t-as="emoji">
|
||||
<span t-att-data-emoji="emoji.source" class="o_mail_compose_message_emoji_img" t-att-title="emoji.description">
|
||||
<t t-raw="emoji.substitution"/>
|
||||
</span>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.ComposeMessage.attachments">
|
||||
<div class="o_mail_chat_im_attachment_files">
|
||||
<t t-foreach="widget.get('attachment_ids')" t-as='attachment'>
|
||||
<div t-attf-class="o_mail_attachment #{attachment.upload ? 'o_mail_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">
|
||||
<div class='o_mail_attachment_name'>
|
||||
<t t-raw='attachment.name'/>
|
||||
</div>
|
||||
</a>
|
||||
<div class="o_mail_attachment_delete">
|
||||
<i class='fa fa-times-circle' title="Delete this attachment" t-att-data-id="attachment.id"/>
|
||||
</div>
|
||||
<div class="o_mail_attachment_progress_bar">
|
||||
Uploading
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.ComposeMessage.mention_menu">
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownMailMentionMenu">
|
||||
<t t-foreach="widget.get('mention_partners')" t-as="partner">
|
||||
<li t-att-data-partner-id="partner.id" t-att-class="'o_mail_mention_proposition ' + (partner_index === 0 ? 'active' : '')">
|
||||
<a href="#">
|
||||
<t t-esc="partner.name"/> (<t t-esc="partner.email || _t('No email')"/>)</a>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.ComposeMessage.mention_tags">
|
||||
<t t-if="widget.get('mention_selected_partners').length">Mention(s):</t>
|
||||
<t t-foreach="widget.get('mention_selected_partners')" t-as="partner">
|
||||
<span class="badge"><t t-esc="partner.name"/></span>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
|
||||
<!--
|
||||
Message Thread
|
||||
-->
|
||||
<t t-name="mail.Thread.messages">
|
||||
<t t-set="current_day" t-value="0"/>
|
||||
<t t-foreach="widget.get('messages')" t-as="message">
|
||||
<div t-if="current_day != message.date.split(' ')[0]" class="o_mail_thread_date_separator">
|
||||
<div class="o_mail_thread_date_wrapper">
|
||||
<div class="o_mail_thread_date_separator_title">
|
||||
<t t-raw="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('LL')"/>
|
||||
</div>
|
||||
</div>
|
||||
<t t-set="current_day" t-value="message.date.split(' ')[0]"/>
|
||||
</div>
|
||||
<div class="o_mail_thread_message" t-att-data-message-id="message.id">
|
||||
<div class="o_mail_thread_message_avatar">
|
||||
<img t-if="message.message_type == 'email' && (!message.author_id || !message.author_id[0]) && message.email_from" src="/mail/static/src/img/email_icon.png" alt="avatar" class="o_mail_thread_avatar img-rounded"/>
|
||||
<img t-if="message.message_type == 'email' && (message.author_id && message.author_id[0]) && !message.email_from" t-att-src="'/web/image/res.partner/'+message.author_id[0]+'/image_small'" class="o_mail_thread_avatar img-rounded"/>
|
||||
<img t-if="message.message_type !== 'email' && (message.author_id && message.author_id[0])" t-att-src="'/web/image/res.partner/'+message.author_id[0]+'/image_small'" class="o_mail_thread_avatar img-rounded"/>
|
||||
<img t-if="message.message_type !== 'email' && (!message.author_id || !message.author_id[0])" src="/mail/static/src/img/smiley/avatar.jpg" class="o_mail_thread_avatar img-rounded"/>
|
||||
</div>
|
||||
<div t-att-class="'o_mail_thread_message_core ' + 'o_mail_thread_message_type_' + message.message_type + ' ' + (message.is_note ? 'o_mail_thread_message_subtype_note' : '')">
|
||||
<!-- actions -->
|
||||
<span class="pull-right">
|
||||
<i t-if="message.is_needaction && widget.options.display_needaction_button" class="fa fa-check o_mail_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_mail_thread_message_star ' + (message.is_starred ? 'o_mail_message_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>
|
||||
<!-- author : partner or email_from -->
|
||||
<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] && message.email_from">
|
||||
<strong><t t-esc="message.email_from"/></strong>
|
||||
</t>
|
||||
<t t-if="message.author_id && !message.author_id[1] && !message.email_from">
|
||||
<strong><t t-esc="widget.options.default_username"/></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') && widget.options.display_document_link">
|
||||
on <a class="o_mail_redirect" t-att-data-oe-model="message.model" t-att-data-oe-id="message.res_id" href="#"><t t-esc="message.record_name"/></a>
|
||||
</t>
|
||||
</p>
|
||||
<!-- tracking message-->
|
||||
<t t-if="message.tracking_value_ids && message.tracking_value_ids.length > 0">
|
||||
<t t-call="mail.Thread.tracking_message">
|
||||
<t t-set="message" t-value="message"/>
|
||||
</t>
|
||||
</t>
|
||||
<!-- normal message -->
|
||||
<t t-if="!(message.tracking_value_ids && message.tracking_value_ids.length > 0)">
|
||||
<t t-if="message.body_short">
|
||||
<div class="o_mail_body_short">
|
||||
<t t-raw="message.body_short"/>
|
||||
<span class="o_mail_expand"><a href="#">... read more</a></span>
|
||||
</div>
|
||||
<div class="o_mail_body_long">
|
||||
<t t-raw="message.body"/>
|
||||
<span class="o_mail_expand"><a href="#">... read less</a></span>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="! message.body_short">
|
||||
<t t-raw="message.body"/>
|
||||
</t>
|
||||
</t>
|
||||
<!-- Attachments -->
|
||||
<t t-foreach="message.attachment_ids" t-as="attachment">
|
||||
<div t-attf-class="o_mail_attachment" 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">
|
||||
<div class='o_mail_attachment_name'>
|
||||
<t t-raw='attachment.name'/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.Thread.load_more">
|
||||
<div t-if="!widget.all_messages_loaded" class="o_mail_thread_show_more">
|
||||
<button class="btn btn-sm btn-link">-------- Show more messages --------</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.Thread.tracking_message">
|
||||
<div>
|
||||
<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 class="o_mail_message_tracking_value"> <t t-esc="value.old_value"/> </span>
|
||||
<span> <i class="fa fa-long-arrow-right"></i> </span>
|
||||
</t>
|
||||
<span class="o_mail_message_tracking_value"> <t t-esc="value.new_value"/> </span>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -3,24 +3,24 @@
|
||||
<data>
|
||||
<template id="assets_backend" name="mail assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/mail/static/src/js/mail_utils.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/mail_followers.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/many2many_tags_email.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/announcement.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/mail/static/src/js/client_action.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/chat_window.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/composer.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/chat_manager.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/chatter.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/thread.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/systray.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/window_manager.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/mail/static/src/less/announcement.less"/>
|
||||
<link rel="stylesheet" href="/mail/static/src/less/mail_thread.less" type="text/less"/>
|
||||
<link rel="stylesheet" href="/mail/static/src/less/mail_chatter.less" type="text/less"/>
|
||||
<link rel="stylesheet" href="/mail/static/src/less/mail_chat_backend.less" type="text/less"/>
|
||||
<link rel="stylesheet" href="/mail/static/src/less/mail_chat_common.less"/>
|
||||
<link rel="stylesheet" href="/mail/static/src/less/mail_followers.less"/>
|
||||
|
||||
<script type="text/javascript" src="/mail/static/src/js/mail_thread.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/mail_chatter.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/mail/static/src/js/mail_chat_common.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/mail_chat_backend.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/mail/static/src/less/client_action.less" type="text/less"/>
|
||||
<link rel="stylesheet" href="/mail/static/src/less/chat_window.less" type="text/less"/>
|
||||
<link rel="stylesheet" href="/mail/static/src/less/composer.less" type="text/less"/>
|
||||
<link rel="stylesheet" href="/mail/static/src/less/chatter.less" type="text/less"/>
|
||||
<link rel="stylesheet" href="/mail/static/src/less/thread.less" type="text/less"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
|
||||
Reference in New Issue
Block a user