[MERGE] mail: refactoring of the chat interface

This merge is a large refactoring of the chat/channel code (client side
code).  A goal is to simplify the logic of updating chat windows,
chatter and channels.

A notable change is that it also disable the livechat js code, because
it was not updated on time.
This commit is contained in:
Géry Debongnie
2015-10-01 14:02:48 +02:00
52 changed files with 3734 additions and 4434 deletions
+2 -1
View File
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
import openerp
from openerp.http import request
from openerp.addons.bus.models.bus import dispatch
@@ -35,4 +34,6 @@ class BusController(openerp.http.Controller):
raise Exception("bus.Bus unavailable")
if [c for c in channels if not isinstance(c, basestring)]:
raise Exception("bus.Bus only string channels are allowed.")
if request.registry.in_test_mode():
raise openerp.exceptions.UserError("bus.Bus not available in test mode")
return self._poll(request.db, channels, last, options)
+12 -1
View File
@@ -1,4 +1,5 @@
odoo.define('bus.bus', function (require) {
"use strict";
var session = require('web.session');
var Widget = require('web.Widget');
@@ -40,11 +41,15 @@ bus.Bus = Widget.extend({
var self = this;
self.activated = true;
var data = {'channels': self.channels, 'last': self.last, 'options' : self.options};
session.rpc('/longpolling/poll', data, {shadow : true}).then(function(result) {
this.current_poll = $.Deferred();
this.current_poll.then(function (result) {
_.each(result, _.bind(self.on_notification, self));
if(!self.stop){
self.poll();
}
});
session.rpc('/longpolling/poll', data, {shadow : true}).then(function(result) {
self.current_poll.resolve(result);
}, function(unused, e) {
// no error popup if request is interrupted or fails for any reason
e.preventDefault();
@@ -58,6 +63,12 @@ bus.Bus = Widget.extend({
}
this.trigger("notification", [notification.channel, notification.message]);
},
restart_poll: function () {
if (this.current_poll) {
this.current_poll.fail();
}
this.poll();
},
add_channel: function(channel){
if(!_.contains(this.channels, channel)){
this.channels.push(channel);
@@ -7,11 +7,14 @@ var user_session = require('web.session');
var time = require('web.time');
var utils = require('web.utils');
var Widget = require('web.Widget');
var mail_chat_common = require('mail.chat_common');
// var mail_chat_common = require('mail.chat_common');
var _t = core._t;
var QWeb = core.qweb;
return {
LivechatButton: Widget,
};
/*
* Conversation Patch
@@ -1,7 +1,8 @@
odoo.define('im_livechat.chat_backend', function (require) {
"use strict";
var mail_chat_backend = require('mail.chat_backend');
// var mail_chat_backend = require('mail.chat_backend');
return;
/**
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>
<t t-extend="mail.chat.ChatMailThread.channels">
<!-- <t t-extend="mail.chat.ChatMailThread.channels">
<t t-jquery="li > a" t-operation="append">
<t t-if="channel.channel_type === 'livechat'">
<span class="fa fa-times pull-right o_mail_chat_channel_unpin" t-att-title="_t('Close')" t-att-data-channel-id="channel.id"/>
@@ -11,12 +11,11 @@
<t t-extend="mail.chat.ChatMailThread">
<t t-jquery=".o_mail_chat_sidebar" t-operation="append">
<!-- Livechat list -->
<h4 class="o_mail_chat_sidebar_title"><t t-esc="_t('Livechat')"/></h4>
<t t-call="mail.chat.ChatMailThread.channels">
<t t-set="channel_slot" t-value="'channel_livechat'"/>
</t>
</t>
</t>
</t> -->
</template>
@@ -125,11 +125,12 @@
<script type="text/javascript" src="/web/static/src/js/framework/core.js"></script>
<!-- Bus, Mail, Livechat -->
<script type="text/javascript" src="/bus/static/src/js/bus.js"></script>
<script type="text/javascript" src="/mail/static/src/js/mail_utils.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/chat_window.js"></script>
<script type="text/javascript" src="/mail/static/src/js/thread.js"></script>
<script type="text/javascript" src="/im_livechat/static/src/js/im_livechat.js"></script>
<link rel="stylesheet" href="/mail/static/src/less/mail_chat_common.less"></link>
<link rel="stylesheet" href="/im_livechat/static/src/css/im_livechat.css"/>
<link rel="stylesheet" href="/mail/static/src/less/chat_window.less"></link>
<link rel="stylesheet" href="/mail/static/src/less/thread.less"></link>
<link rel="stylesheet" href="/im_livechat/static/src/less/im_livechat.less"/>
</template>
<!-- the js code to initialize the LiveSupport object -->
+6 -5
View File
@@ -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',
],
}
+448
View File
@@ -0,0 +1,448 @@
odoo.define('mail.chat_manager', function (require) {
"use strict";
var bus = require('bus.bus').bus;
var core = require('web.core');
var data = require('web.data');
var Model = require('web.Model');
var session = require('web.session');
var web_client = require('web.web_client');
var _t = core._t;
var LIMIT = 20;
var MessageModel = new Model('mail.message', session.context);
var ChannelModel = new Model('mail.channel', session.context);
// Private model
//----------------------------------------------------------------------------------
var messages = [];
var channels = [];
var emojis = [];
var emoji_substitutions = {};
var needaction_counter = 0;
// Message and channel manipulation helpers
//----------------------------------------------------------------------------------
// options: channel_id, silent
function add_message (data, options) {
options = options || {};
var msg = _.findWhere(messages, { id: data.id });
if (!msg) {
msg = make_message(data, options.channel_id);
// Keep the array ordered by date when inserting the new message
messages.splice(_.sortedIndex(messages, msg, 'date'), 0, msg);
if (options.channel_id) {
var channel = _.findWhere(channels, {id: options.channel_id});
if (channel.hidden) {
channel.hidden = false;
chat_manager.bus.trigger('new_channel', channel);
}
if ((channel.type === "dm") && (options.show_notification)) {
var query = { is_displayed: false };
chat_manager.bus.trigger('anyone_listening', channel, query);
if (!query.is_displayed) {
web_client.do_notify(_t('New message'), _t("You received a message from ") + msg.author_id[1]);
}
}
}
if (!options.silent) {
chat_manager.bus.trigger('new_message', msg);
}
} else if (msg && options.channel_id !== undefined) {
add_channel_to_message(msg, options.channel_id);
}
return msg;
}
function make_message (data, channel_id) {
var msg = {
id: data.id,
author_id: data.author_id,
body: data.body,
date: data.date,
message_type: data.message_type,
is_note: data.is_note,
attachment_ids: data.attachment_ids,
subject: data.subject,
email_from: data.email_from,
record_name: data.record_name,
tracking_value_ids: data.tracking_value_ids,
channel_ids: (channel_id !== undefined ? [channel_id] : []),
model: data.model,
res_id: data.res_id,
};
_.each(_.keys(emoji_substitutions), function(key){
var escaped_key = String(key).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1'); //]
var regexp = new RegExp("(?:^|\\s|<[a-z]*>)(" + escaped_key + ")(?:\\s|$|</[a-z]*>)");
msg.body = msg.body.replace(regexp, ' <span class="o_mail_emoji">'+emoji_substitutions[key]+'</span> ');
});
function property_descr(channel) {
return {
enumerable: true,
get: function () {
return _.contains(msg.channel_ids, channel);
},
set: function (bool) {
if (bool) {
add_channel_to_message(msg, channel);
} else {
msg.channel_ids = _.without(msg.channel_ids, channel);
}
}
};
}
Object.defineProperties(msg, {
is_starred: property_descr("channel_starred"),
is_needaction: property_descr("channel_inbox"),
});
if (_.contains(data.needaction_partner_ids, session.partner_id)) {
msg.is_needaction = true;
}
if (_.contains(data.starred_partner_ids, session.partner_id)) {
msg.is_starred = true;
}
return msg;
}
function add_channel_to_message (message, channel_id) {
message.channel_ids.push(channel_id);
message.channel_ids = _.uniq(message.channel_ids);
}
function post_channel_message (data) {
return ChannelModel.call('message_post', [data.channel_id], {
message_type: 'comment',
content_subtype: 'html',
partner_ids: data.partner_ids,
body: data.content,
subtype: 'mail.mt_comment',
attachment_ids: data.attachment_ids,
});
}
function post_document_message (model_name, res_id, data) {
var values = {
attachment_ids: data.attachment_ids,
body: data.content,
content_subtype: data.content_subtype,
message_type: data.message_type,
partner_ids: data.partner_ids,
subtype: data.subtype,
subtype_id: data.subtype_id,
};
var model = new Model(model_name);
return model.call('message_post', [res_id], values).then(function (msg_id) {
return MessageModel.call('message_format', [msg_id]).then(function (msgs) {
msgs[0].model = model_name;
msgs[0].res_id = res_id;
add_message(msgs[0]);
});
});
}
function add_channel (data, options) {
options = typeof options === "object" ? options : {};
var channel = _.findWhere(channels, {id: data.id});
if (channel) {
if (channel.is_folded !== (data.state === "folded")) {
channel.is_folded = (data.state === "folded");
chat_manager.bus.trigger("channel_toggle_fold", channel);
}
} else {
channel = make_channel(data, options);
channels.push(channel);
if (!options.silent) {
chat_manager.bus.trigger("new_channel", channel);
}
if (channel.is_detached) {
chat_manager.bus.trigger("open_chat", channel);
}
}
}
function make_channel (data, options) {
var channel = {
id: data.id,
name: data.name,
type: data.type || "public",
all_history_loaded: false,
uuid: data.uuid,
is_detached: data.is_minimized,
is_folded: data.state === "folded",
loaded: false,
autoswitch: 'autoswitch' in options ? options.autoswitch : true,
hidden: options.hidden,
};
if (data.public === "private") {
channel.type = "private";
}
if ('direct_partner' in data) {
channel.type = "dm";
channel.name = data.direct_partner[0].name;
channel.status = data.direct_partner[0].im_status;
}
return channel;
}
// options: domain, load_more
function fetch_from_channel (channel_id, options) {
var domain =
(channel_id === "channel_inbox") ? [['needaction', '=', true]] :
(channel_id === "channel_starred") ? [['starred', '=', true]] :
[['channel_ids', 'in', channel_id]];
options = options || {};
if (options.domain) {
domain = new data.CompoundDomain(domain, options.domain || []);
}
if (options.load_more) {
var min_message_id = _.chain(messages)
.filter(function (msg) { return _.contains(msg.channel_ids, channel_id); })
.pluck("id")
.min()
.value();
domain = new data.CompoundDomain([['id', '<', min_message_id]], domain);
}
return MessageModel.call('message_fetch', [domain], {limit: LIMIT}).then(function (msgs) {
var channel = _.findWhere(channels, {id: channel_id});
channel.loaded = true;
if (!channel.all_history_loaded) {
channel.all_history_loaded = msgs.length < LIMIT;
}
_.each(msgs, function (msg) {
add_message(msg, {channel_id: channel_id, silent: true});
});
return _.filter(messages, function (m) {
return _.contains(m.channel_ids, channel_id);
});
});
}
// options: force_fetch
function fetch_document_messages (ids, options) {
var loaded_msgs = _.filter(messages, function (message) {
return _.contains(ids, message.id);
});
var loaded_msg_ids = _.pluck(loaded_msgs, 'id');
options = options || {};
if (options.force_fetch || _.difference(ids.slice(0, LIMIT), loaded_msg_ids).length) {
var ids_to_load = _.difference(ids, loaded_msg_ids).slice(0, LIMIT);
return MessageModel.call('message_format', [ids_to_load]).then(function (msgs) {
var processed_msgs = [];
_.each(msgs, function (msg) {
processed_msgs.push(add_message(msg, {silent: true}));
});
return _.sortBy(loaded_msgs.concat(processed_msgs), function (msg) {
return msg.date;
});
});
} else {
return $.when(loaded_msgs);
}
}
// Public interface
//----------------------------------------------------------------------------------
var chat_manager = {
post_message: post_channel_message,
post_message_in_document: post_document_message,
get_messages: function (options) {
if ('channel_id' in options) { // channel message
var channel = _.findWhere(channels, {id: options.channel_id});
if (channel.loaded) {
return $.when(_.filter(messages, function (message) {
return _.contains(message.channel_ids, channel.id);
}));
} else {
return fetch_from_channel(options.channel_id);
}
} else { // chatter message
}
},
fetch: function (channel_id, domain) {
return fetch_from_channel(channel_id, {domain: domain});
},
fetch_more: function (channel_id, domain) {
return fetch_from_channel(channel_id, {domain: domain, load_more: true});
},
/**
* Fetches chatter messages from their ids
*/
fetch_messages: function (message_ids, options) {
return fetch_document_messages(message_ids, options);
},
toggle_star_status: function (message_id) {
var msg = _.findWhere(messages, { id: message_id });
return MessageModel.call('set_message_starred', [[message_id], !msg.is_starred]).then(function () {
msg.is_starred = !msg.is_starred;
chat_manager.bus.trigger('update_message', msg);
});
},
mark_as_read: function (message_id) {
return MessageModel.call('set_message_done', [[message_id]]).then(function () {
var message = _.findWhere(messages, { id: message_id });
message.channel_ids = _.without(message.channel_ids, "channel_inbox");
chat_manager.bus.trigger('update_message', message);
needaction_counter = needaction_counter - 1;
chat_manager.bus.trigger('update_needaction', needaction_counter);
});
},
get_channels: function () {
return _.clone(channels);
},
get_channel: function (id) {
return _.findWhere(channels, {id: id}) || channels[0];
},
all_history_loaded: function (channel_id) {
return this.get_channel(channel_id).all_history_loaded;
},
get_emojis: function() {
return emojis;
},
get_needaction_counter: function () {
return needaction_counter;
},
detach_channel: function (channel_id) {
var channel = this.get_channel(channel_id);
return ChannelModel.call("channel_minimize", [channel.uuid, true]);
},
remove_chatter_messages: function (model) {
messages = _.reject(messages, function (message) {
return message.channel_ids.length === 0 && message.model === model;
});
},
bus: new core.Bus(),
create_channel: function (name, type) {
var method = type === "dm" ? "channel_get" : "channel_create";
var args = type === "dm" ? [[name]] : [name, type];
return ChannelModel
.call(method, args)
.then(add_channel)
.then(function () {
bus.restart_poll();
});
},
join_channel: function (channel_id) {
return ChannelModel
.call('channel_join_and_get_info', [[channel_id]])
.then(add_channel);
},
unsubscribe: function (channel_id) {
var def;
var channel = chat_manager.get_channel(channel_id);
if (channel.type === "dm") {
def = ChannelModel.call('channel_pin', [channel.uuid, false]);
} else {
def = ChannelModel.call('action_unfollow', [[channel_id]]);
}
return def.then(function () {
channels = _.without(channels, channel);
});
},
close_chat_session: function (channel_id) {
var channel = _.findWhere(channels, {id: channel_id});
ChannelModel.call("channel_fold", [], {uuid : channel.uuid, state : "closed"});
},
fold_channel: function (channel_id) {
var channel = _.findWhere(channels, {id: channel_id});
return ChannelModel.call("channel_fold", [], {uuid : channel.uuid}).then(function () {
channel.is_folded = !channel.is_folded;
});
},
};
// Initialization
// ---------------------------------------------------------------------------------
function init () {
add_channel({
id: "channel_inbox",
name: _t("Inbox"),
type: "static"
});
add_channel({
id: "channel_starred",
name: _t("Starred"),
type: "static"
});
var load_channels = session.rpc('/mail/client_action').then(function (result) {
_.each(result.channel_slots.channel_channel, add_channel);
_.each(result.channel_slots.channel_private_group, add_channel);
_.each(result.channel_slots.channel_direct_message, add_channel);
needaction_counter = result.needaction_inbox_counter;
});
var load_emojis = session.rpc("/mail/chat_init").then(function (result) {
emojis = result.emoji;
_.each(emojis, function(emoji) {
emoji_substitutions[emoji.source] = emoji.substitution;
});
});
bus.on('notification', null, function (notification) {
var model = notification[0][1];
if (model === 'mail.channel') {
// new message in a channel
var message = notification[1];
var channel_id = message.channel_ids[0];
message = add_message(message, { channel_id: channel_id, show_notification: true} );
if (_.contains(message.channel_ids, 'channel_inbox')) {
needaction_counter = needaction_counter + 1;
chat_manager.bus.trigger('update_needaction', needaction_counter);
}
}
if (model === 'res.partner') {
var chat_session = notification[1];
if ((chat_session.channel_type === "channel") && (chat_session.public === "private") && (chat_session.state === "open")) {
add_channel(chat_session, {autoswitch: false});
if (!chat_session.is_minimized) {
web_client.do_notify(_t("Private Channel"), _t("You have been invited to: ") + chat_session.name);
}
}
// partner specific change (open a detached window for example)
if ((chat_session.state === "open") || (chat_session.state === "folded")) {
add_channel(chat_session, {autoswitch: false, silent: true, hidden: true});
if (chat_session.is_minimized) {
chat_manager.bus.trigger("open_chat", chat_session);
}
}
if (chat_session.state === "closed") {
chat_manager.bus.trigger("close_chat", chat_session);
}
}
});
return $.when(load_channels, load_emojis).then(function () {
bus.start_polling();
return chat_manager;
});
}
return init();
});
+77
View File
@@ -0,0 +1,77 @@
odoo.define('mail.ChatWindow', function (require) {
"use strict";
var ChatThread = require('mail.ChatThread');
var core = require('web.core');
var Widget = require('web.Widget');
var _t = core._t;
return Widget.extend({
template: "mail.ChatWindow",
events: {
"keydown .o_chat_input": "on_keydown",
"click .o_chat_window_close": "on_click_close",
"click .o_chat_title": "on_click_fold",
},
init: function (parent, channel_id, title, is_folded) {
this._super(parent);
this.title = title;
this.channel_id = channel_id;
this.placeholder = _t("Say something");
this.folded = is_folded;
},
start: function () {
this.$content = this.$('.o_chat_content');
this.$input = this.$('.o_chat_input input');
this.thread = new ChatThread(this, {display_avatar: false});
this.thread.on('toggle_star_status', this, function (message_id) {
this.trigger('toggle_star_status', message_id);
});
this.fold();
var def = this.thread.appendTo(this.$content);
return $.when(this._super(), def);
},
render: function (messages) {
this.thread.render(messages, {display_load_more: false});
},
scrollBottom: function () {
this.$content.scrollTo('max');
},
fold: function () {
this.$el.animate({
height: this.folded ? "28px" : "333px"
});
},
toggle_fold: function () {
this.folded = !this.folded;
this.fold();
},
on_keydown: function (event) {
if (event.which === $.ui.keyCode.ENTER) {
var message = {
content: this.$input.val(),
attachment_ids: [],
partner_ids: [],
channel_id: this.channel_id,
};
this.$input.val('');
this.trigger('post_message', message);
}
},
on_click_close: function (event) {
event.stopPropagation();
event.preventDefault();
this.trigger("close_chat_session");
},
on_click_fold: function () {
this.trigger("fold_channel", this.channel_id);
this.toggle_fold();
},
});
});
+897
View File
@@ -0,0 +1,897 @@
odoo.define('mail.Chatter', function (require) {
"use strict";
var chat_manager = require('mail.chat_manager');
var ChatComposer = require('mail.ChatComposer');
var ChatThread = require('mail.ChatThread');
var ajax = require('web.ajax');
var core = require('web.core');
var data = require('web.data');
var Dialog = require('web.Dialog');
var form_common = require('web.form_common');
var session = require('web.session');
var web_client = require('web.web_client');
var _t = core._t;
var qweb = core.qweb;
// -----------------------------------------------------------------------------
// Followers Widget ('mail_followers' widget)
//
// Since it is displayed on a form view, it extends 'AbstractField' widget.
//
// Note: the followers widget is moved inside the chatter by the chatter itself
// for layout purposes.
// -----------------------------------------------------------------------------
var Followers = form_common.AbstractField.extend({
template: 'mail.Followers',
init: function () {
this._super.apply(this, arguments);
this.image = this.node.attrs.image || 'image_small';
this.comment = this.node.attrs.help || false;
this.ds_model = new data.DataSetSearch(this, this.view.model);
this.ds_users = new data.DataSetSearch(this, 'res.users');
this.value = [];
this.followers = [];
this.data_subtype = {};
this.view_is_editable = this.__parentedParent.is_action_enabled('edit');
},
start: function () {
// use actual_mode property on view to know if the view is in create mode anymore
this.view.on("change:actual_mode", this, this.on_check_visibility_mode);
this.on_check_visibility_mode();
this.reinit();
this.bind_events();
return this._super();
},
on_check_visibility_mode: function () {
this.set({"force_invisible": this.view.get("actual_mode") === "create"});
},
set_value: function (_value) {
this.value = _value;
this._super(_value);
},
reinit: function () {
this.data_subtype = {};
this.message_is_follower = undefined;
this.display_buttons();
},
bind_events: function () {
var self = this;
// event: click on '(Un)Follow' button, that toggles the follow for uid
this.$el.on('click', '.o_followers_follow_button', function () {
if ($(this).hasClass('o_followers_notfollow')) {
self.do_follow();
} else {
self.do_unfollow({user_ids: [session.uid]});
}
});
// event: click on a subtype, that (un)subscribe for this subtype
this.$el.on('click', '.o_subtypes_list input', function(event) {
event.stopPropagation();
self.do_update_subscription(event);
var $list = self.$('.o_subtypes_list');
if (!$list.hasClass('open')) {
$list.addClass('open');
}
if (self.$('.o_subtypes_list ul')[0].children.length < 1) {
$list.removeClass('open');
}
});
// event: click on 'invite' button, that opens the invite wizard
this.$el.on('click', '.o_add_follower', function(event) {
event.preventDefault();
self.on_invite_follower(false);
});
this.$el.on('click', '.o_add_follower_channel', function(event) {
event.preventDefault();
self.on_invite_follower(true);
});
// event: click on 'edit_subtype(pencil)' button to edit subscription
this.$el.on('click', '.o_edit_subtype', self.on_edit_subtype);
this.$el.on('click', '.o_remove_follower', self.on_remove_follower);
this.$el.on('click', 'a[data-partner]', self.on_follower_clicked);
},
on_edit_subtype: function (event) {
var self = this;
var $currentTarget = $(event.currentTarget);
var follower_id = $currentTarget.data('oe-id');
var is_channel = $currentTarget.data('oe-model') === 'mail.channel';
self.dialog = new Dialog(this, {
size: 'small',
title: _t('Edit Subscription of ') + $currentTarget.siblings('a').text(),
buttons: [
{
text: _t("Apply"),
classes: 'btn-primary',
click: function () {
self.do_update_subscription(event, follower_id, is_channel);
},
close: true
},
{
text: _t("Cancel"),
close: true,
},
],
}).open();
return self.fetch_subtypes($currentTarget.data('id'));
},
on_invite_follower: function (channel_only) {
var self = this;
var action = {
type: 'ir.actions.act_window',
res_model: 'mail.wizard.invite',
view_mode: 'form',
view_type: 'form',
views: [[false, 'form']],
name: _t('Invite Follower'),
target: 'new',
context: {
'default_res_model': this.view.dataset.model,
'default_res_id': this.view.datarecord.id,
'mail_invite_follower_channel_only': channel_only,
},
};
this.do_action(action, {
on_close: function () {
self.read_value();
},
});
},
on_remove_follower: function (event) {
var res_model = $(event.target).parent().find('a').data('oe-model');
var res_id = $(event.target).parent().find('a').data('oe-id');
if (res_model === 'res.partner') {
return this.do_unfollow({partner_ids: [res_id]});
} else {
return this.do_unfollow({channel_ids: [res_id]});
}
},
on_follower_clicked: function (event) {
event.preventDefault();
var partner_id = $(event.target).data('partner');
var state = {
'model': 'res.partner',
'id': partner_id,
'title': this.record_name
};
web_client.action_manager.do_push_state(state);
var action = {
type:'ir.actions.act_window',
view_type: 'form',
view_mode: 'form',
res_model: 'res.partner',
views: [[false, 'form']],
res_id: partner_id,
};
this.do_action(action);
},
read_value: function () {
var self = this;
return this.ds_model.read_ids([this.view.datarecord.id], ['message_follower_ids'])
.then(function (results) {
self.value = results[0].message_follower_ids;
self.render_value();
});
},
render_value: function () {
this.reinit();
return this.fetch_followers(this.value);
},
fetch_followers: function (value_) {
this.value = value_ || [];
return ajax.jsonRpc('/mail/read_followers', 'call', {'follower_ids': this.value})
.then(this.proxy('display_followers'), this.proxy('fetch_generic'))
.then(this.proxy('display_buttons'))
.then(this.proxy('fetch_subtypes'));
},
/** Read on res.partner failed: fall back on a generic case
- fetch current user partner_id (call because no other smart solution currently) FIXME
- then display a generic message about followers */
fetch_generic: function () {
var self = this;
return this.ds_users.call('read', [[session.uid], ['partner_id']])
.then(function (results) {
var pid = results[0].partner_id[0];
self.message_is_follower = (_.indexOf(self.value, pid) !== -1);
})
.then(self.proxy('display_generic'));
},
_format_followers: function (count){
var str = '';
if(count <= 0){
str = _t('No follower');
}else if(count === 1){
str = _t('One follower');
}else{
str = ''+count+' '+_t('followers');
}
return str;
},
/* Display generic info about follower, for people not having access to res_partner */
display_generic: function () {
this.$('.o_followers_list').empty();
this.$('.o_followers_count').html(this._format_followers(this.value.length));
},
/** Display the followers */
display_followers: function (records) {
var self = this;
this.followers = records || this.followers;
// clean and display title
var $followers_list = this.$('.o_followers_list').empty();
this.$('.o_followers_count').html(this._format_followers(this.followers.length));
var user_follower = _.filter(this.followers, function (rec) { return rec.is_uid;});
this.message_is_follower = user_follower.length >= 1;
this.follower_id = this.message_is_follower ? user_follower[0].id : undefined;
// render the dropdown content
$(qweb.render('mail.Followers.add_more', {'widget': self})).appendTo($followers_list);
var $follower_li;
_(this.followers).each(function (record) {
$follower_li = $(qweb.render('mail.Followers.partner', {
'record': _.extend(record, {'avatar_url': '/web/image/' + record.res_model + '/' + record.res_id + '/image_small'}),
'widget': self})
);
$follower_li.appendTo($followers_list);
// On mouse-enter it will show the edit_subtype pencil.
if (record.is_editable) {
$follower_li.on('mouseenter mouseleave', function(e) {
$(e.currentTarget).find('.o_edit_subtype').toggleClass('hide', e.type === 'mouseleave');
});
}
});
},
display_buttons: function () {
if (this.message_is_follower) {
this.$('button.o_followers_follow_button').removeClass('o_followers_notfollow').addClass('o_followers_following');
} else {
this.$('button.o_followers_follow_button').removeClass('o_followers_following').addClass('o_followers_notfollow');
}
},
/** Fetch subtypes, only if current user is follower */
fetch_subtypes: function (follower_id) {
var self = this;
var dialog = false;
if (follower_id) {
dialog = true;
}
else {
this.$('.o_subtypes_list ul').empty();
if (! this.message_is_follower) {
this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', true);
return;
}
else {
this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', false);
}
}
if (this.follower_id) {
return ajax.jsonRpc('/mail/read_subscription_data', 'call', {
res_model: this.view.model,
res_id: this.view.datarecord.id,
follower_id: follower_id,
}).then(function (data) {
self.display_subtypes(data, dialog);
});
} else {
return $.Deferred().resolve();
}
},
/** Display subtypes: {'name': default, followed} */
display_subtypes:function (data, dialog) {
var self = this;
var old_parent_model;
var $list;
if (dialog) {
$list = this.dialog.$el;
} else {
$list = this.$('.o_subtypes_list ul');
}
$list.empty();
this.data_subtype = data;
this.records_length = $.map(data, function(value, index) { return index; }).length;
if (this.records_length > 1) {
self.display_followers();
}
_.each(data, function (record) {
if (old_parent_model !== record.parent_model && old_parent_model !== undefined) {
$list.append($('<li>').addClass('divider'));
}
old_parent_model = record.parent_model;
record.followed = record.followed || undefined;
$(qweb.render('mail.Followers.subtype', {'record': record,
'dialog': dialog}))
.appendTo($list);
});
},
do_follow: function () {
var context = new data.CompoundContext(this.build_context(), {});
this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', false);
this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id],
[session.uid],
undefined,
context])
.then(this.proxy('read_value'));
_.each(this.$('.o_subtypes_list input'), function (record) {
$(record).attr('checked', 'checked');
});
},
/**
* Remove users, partners, or channels from the followers
* @param {Array} [ids.user_ids] the user ids
* @param {Array} [ids.partner_ids] the partner ids
* @param {Array} [ids.channel_ids] the channel ids
*/
do_unfollow: function (ids) {
if (confirm(_t("Warning! \n If you remove a follower, he won't be notified of any email or discussion on this document. Do you really want to remove this follower ?"))) {
_(this.$('.o_subtype_checkbox')).each(function (record) {
$(record).attr('checked',false);
});
this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', true);
var context = new data.CompoundContext(this.build_context(), {});
if (ids.partner_ids || ids.channel_ids) {
return this.ds_model.call(
'message_unsubscribe', [
[this.view.datarecord.id],
ids.partner_ids,
ids.channel_ids,
context]
).then(this.proxy('read_value'));
} else {
return this.ds_model.call(
'message_unsubscribe_users', [
[this.view.datarecord.id],
ids.user_ids,
context]
).then(this.proxy('read_value'));
}
}
return false;
},
do_update_subscription: function (event, follower_id, is_channel) {
var self = this;
var kwargs = {};
var ids = {};
var action_subscribe;
var subtypes;
this.data_subtype = {};
if (follower_id !== undefined) {
// Subtypes edited from the modal
action_subscribe = 'message_subscribe';
subtypes = this.dialog.$('input[type="checkbox"]');
if (is_channel) {
ids.channel_ids = [follower_id];
} else {
ids.partner_ids = [follower_id];
}
} else {
action_subscribe = 'message_subscribe_users';
subtypes = this.$('.o_followers_actions input[type="checkbox"]');
ids.user_ids = [session.uid];
}
kwargs = _.extend(kwargs, ids);
// Get the subtype ids
var checklist = [];
_(subtypes).each(function (record) {
if ($(record).is(':checked')) {
checklist.push(parseInt($(record).data('id')));
}
});
kwargs.subtype_ids = checklist;
// If no more subtype followed, unsubscribe the follower
if (!checklist.length) {
if (!this.do_unfollow(ids)) {
$(event.target).attr("checked", "checked");
} else {
self.$('.o_subtypes_list ul').empty();
}
} else {
kwargs.context = new data.CompoundContext(this.build_context(), {});
return this.ds_model._model.call(action_subscribe, [[this.view.datarecord.id]], kwargs).then(this.proxy('read_value'));
}
},
});
// -----------------------------------------------------------------------------
// Chat Composer for the Chatter
//
// Extends the basic Composer Widget to add 'suggested partner' layer (open
// popup when suggested partner is selected without email, or other
// informations), and the button to open the full composer wizard.
// -----------------------------------------------------------------------------
var ChatterComposer = ChatComposer.extend({
template: 'mail.chatter.ChatComposer',
init: function (parent, dataset, options) {
this._super(parent, options);
this.thread_dataset = dataset;
this.suggested_partners = [];
this.options = _.defaults(this.options, {
display_mode: 'textarea',
record_name: false,
is_log: false,
internal_subtypes: [],
});
this.events = _.extend(this.events, {
'click .o_composer_button_full_composer': 'on_open_full_composer',
});
},
willStart: function () {
if (this.options.is_log) {
return this._super.apply(this, arguments);
}
return $.when(this._super.apply(this, arguments), this.message_get_suggested_recipients());
},
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
autosize(self.$input);
});
},
preprocess_message: function () {
var self = this;
var def = $.Deferred();
this._super().then(function (message) {
message = _.extend(message, {
subtype_id: false,
subtype: 'mail.mt_comment',
message_type: 'comment',
content_subtype: 'html',
});
// Subtype
if (self.options.is_log) {
var subtype_id = parseInt(self.$('.o_chatter_composer_subtype_select').val());
if (_.indexOf(_.pluck(self.options.internal_subtypes, 'id'), subtype_id) === -1) {
message.subtype = 'mail.mt_note';
} else {
message.subtype_id = subtype_id;
}
}
// Partner_ids
if (!self.options.is_log) {
var checked_suggested_partners = self.get_checked_suggested_partners();
self.check_suggested_partners(checked_suggested_partners).done(function (partner_ids) {
message.partner_ids = (message.partner_ids || []).concat(partner_ids);
// update context
message.context = _.defaults(self.context, {
mail_post_autofollow: true,
mail_post_autofollow_partner_ids: partner_ids,
});
def.resolve(message);
});
} else {
def.resolve(message);
}
});
return def;
},
/**
* Override keydown event handler to go to next line on ENTER and send message on CTRL+ENTER
*/
on_keydown: function (event) {
if (!event.ctrlKey && (event.keyCode === $.ui.keyCode.ENTER) && !this.get('mention_partners').length) {
return;
}
this._super(event);
},
message_get_suggested_recipients: function () {
var self = this;
var email_addresses = _.pluck(this.suggested_partners, 'email_address');
return this.thread_dataset
.call('message_get_suggested_recipients', [[this.context.default_res_id], this.context])
.done(function (suggested_recipients) {
var thread_recipients = suggested_recipients[self.context.default_res_id];
_.each(thread_recipients, function (recipient) {
var parsed_email = parse_email(recipient[1]);
if (_.indexOf(email_addresses, parsed_email[1]) === -1) {
self.suggested_partners.push({
checked: true,
partner_id: recipient[0],
full_name: recipient[1],
name: parsed_email[0],
email_address: parsed_email[1],
reason: recipient[2],
});
}
});
});
},
/**
* Get the list of selected suggested partners
* @returns Array() : list of 'recipient' selected partners (may not be created in db)
**/
get_checked_suggested_partners: function () {
var self = this;
var checked_partners = [];
this.$('.o_composer_suggested_partners input:checked').each(function() {
var full_name = $(this).data('fullname');
checked_partners = checked_partners.concat(_.filter(self.suggested_partners, function(item) {
return full_name === item.full_name;
}));
});
return checked_partners;
},
/**
* Check the additionnal partners (not necessary registered partners), and open a popup form view
* for the ones who informations is missing.
* @param Array : list of 'recipient' partners to complete informations or validate
* @returns Deferred resolved with the list of checked suggested partners (real partner)
**/
check_suggested_partners: function (checked_suggested_partners) {
var self = this;
var check_done = $.Deferred();
var recipients = _.filter(checked_suggested_partners, function (recipient) { return recipient.checked; });
var recipients_to_find = _.filter(recipients, function (recipient) { return (! recipient.partner_id); });
var names_to_find = _.pluck(recipients_to_find, 'full_name');
var recipients_to_check = _.filter(recipients, function (recipient) { return (recipient.partner_id && ! recipient.email_address); });
var recipient_ids = _.pluck(_.filter(recipients, function (recipient) { return recipient.partner_id && recipient.email_address; }), 'partner_id');
var names_to_remove = [];
var recipient_ids_to_remove = [];
// have unknown names -> call message_get_partner_info_from_emails to try to find partner_id
var find_done = $.Deferred();
if (names_to_find.length > 0) {
find_done = self.thread_dataset.call('message_partner_info_from_emails', [[this.context.default_res_id], names_to_find]);
} else {
find_done.resolve([]);
}
// for unknown names + incomplete partners -> open popup - cancel = remove from recipients
$.when(find_done).pipe(function (result) {
var emails_deferred = [];
var recipient_popups = result.concat(recipients_to_check);
_.each(recipient_popups, function (partner_info) {
var deferred = $.Deferred();
emails_deferred.push(deferred);
var partner_name = partner_info.full_name;
var partner_id = partner_info.partner_id;
var parsed_email = parse_email(partner_name);
var dialog = new form_common.FormViewDialog(self, {
res_model: 'res.partner',
res_id: partner_id,
context: {
force_email: true,
ref: "compound_context",
default_name: parsed_email[0],
default_email: parsed_email[1],
},
title: _t("Please complete partner's informations"),
}).open();
dialog.on('closed', self, function () {
deferred.resolve();
});
dialog.view_form.on('on_button_cancel', self, function () {
names_to_remove.push(partner_name);
if (partner_id) {
recipient_ids_to_remove.push(partner_id);
}
});
});
$.when.apply($, emails_deferred).then(function () {
var new_names_to_find = _.difference(names_to_find, names_to_remove);
find_done = $.Deferred();
if (new_names_to_find.length > 0) {
find_done = self.thread_dataset.call('message_partner_info_from_emails', [[self.context.default_res_id], new_names_to_find, true]);
} else {
find_done.resolve([]);
}
$.when(find_done).pipe(function (result) {
var recipient_popups = result.concat(recipients_to_check);
_.each(recipient_popups, function (partner_info) {
if (partner_info.partner_id && _.indexOf(partner_info.partner_id, recipient_ids_to_remove) === -1) {
recipient_ids.push(partner_info.partner_id);
}
});
}).pipe(function () {
check_done.resolve(recipient_ids);
});
});
});
return check_done;
},
on_open_full_composer: function() {
if (!this.do_check_attachment_upload()){
return false;
}
var self = this;
var recipient_done = $.Deferred();
if (this.options.is_log) {
recipient_done.resolve([]);
} else {
var checked_suggested_partners = this.get_checked_suggested_partners();
recipient_done = this.check_suggested_partners(checked_suggested_partners);
}
recipient_done.then(function (partner_ids) {
var context = {
default_parent_id: self.id,
default_body: get_text2html(self.$input.val()),
default_attachment_ids: _.pluck(self.get('attachment_ids'), 'id'),
default_partner_ids: partner_ids,
default_is_log: self.options.is_log,
mail_post_autofollow: true,
mail_post_autofollow_partner_ids: partner_ids,
};
if (self.context.default_model && self.context.default_res_id) {
context.default_model = self.context.default_model;
context.default_res_id = self.context.default_res_id;
}
self.do_action({
type: 'ir.actions.act_window',
res_model: 'mail.compose.message',
view_mode: 'form',
view_type: 'form',
views: [[false, 'form']],
target: 'new',
context: context,
}, {
on_close: function() {
self.trigger('need_refresh');
},
});
});
}
});
// -----------------------------------------------------------------------------
// Document Chatter ('mail_thread' widget)
//
// Since it is displayed on a form view, it extends 'AbstractField' widget.
// -----------------------------------------------------------------------------
var Chatter = form_common.AbstractField.extend({
template: 'mail.Chatter',
events: {
"click .o_mail_redirect": "on_click_redirect",
"click .o_chatter_button_new_message": "on_open_composer_new_message",
"click .o_chatter_button_log_note": "on_open_composer_log_note",
},
init: function () {
this._super.apply(this, arguments);
this.messages = [];
this.model = this.view.dataset.model;
this.res_id = undefined;
this.context = this.options.context || {};
},
start: function () {
var self = this;
// Move the follower's widget (if any) inside the chatter
var followers = this.field_manager.fields.message_follower_ids;
if (followers) {
this.$('.o_chatter_topbar').append(followers.$el);
}
this.thread = new ChatThread(this, {
display_order: ChatThread.ORDER.DESC,
display_document_link: false,
});
this.thread.on('load_more_messages', this, this.load_more_messages);
this.thread.on('toggle_star_status', this, function (message_id) {
chat_manager.toggle_star_status(message_id);
});
var def1 = this._super.apply(this, arguments);
var def2 = this.thread.appendTo(this.$el);
return $.when(def1, def2).then(function () {
chat_manager.bus.on('new_message', self, self.on_new_message);
chat_manager.bus.on('update_message', self, self.on_update_message);
});
},
fetch_and_render_thread: function (ids, options) {
var self = this;
return chat_manager.fetch_messages(ids, options).then(function (raw_messages) {
self.thread.render(raw_messages, {display_load_more: raw_messages.length < ids.length});
});
},
on_post_message: function (message) {
var self = this;
chat_manager
.post_message_in_document(this.model, this.res_id, message)
.then(function () {
self.close_composer();
})
.fail(function () {
// todo: display notification
});
},
/**
* When a message is correctly posted, fetch its data to render it
* @param {Number} message_id : the identifier of the new posted message
* @returns {Deferred}
*/
on_new_message: function (message) {
if (message.model === this.model && message.res_id === this.res_id) {
this.msg_ids.unshift(message.id);
this.fetch_and_render_thread(this.msg_ids);
}
},
on_update_message: function (message) {
if (message.model === this.model && message.res_id === this.res_id) {
this.fetch_and_render_thread(this.msg_ids);
}
},
on_click_redirect: function (event) {
event.preventDefault();
this.do_action({
type:'ir.actions.act_window',
view_type: 'form',
view_mode: 'form',
res_model: $(event.target).data('oe-model'),
views: [[false, 'form']],
res_id: $(event.target).data('oe-id'),
});
},
load_more_messages: function () {
this.fetch_and_render_thread(this.msg_ids, {force_fetch: true});
},
/**
* The value of the field has change (modification or switch to next document). Form view is
* not re-rendered, simply updated. A re-fetch is needed.
* @override
*/
render_value: function () {
// update context
this.context = _.extend({
default_res_id: this.view.datarecord.id || false,
default_model: this.view.model || false,
}, this.options.context || {});
this.thread_dataset = this.view.dataset;
this.res_id = this.view.datarecord.id;
this.record_name = this.view.datarecord.display_name;
this.msg_ids = this.get_value() || [];
// destroy current composer, if any
if (this.composer) {
this.composer.destroy();
}
// fetch and render messages of current document
return this.fetch_and_render_thread(this.msg_ids);
},
// composer toggle
on_open_composer_new_message: function () {
this.open_composer();
},
on_open_composer_log_note: function () {
this.open_composer({is_log: true});
},
open_composer: function (options) {
var self = this;
var old_composer = this.composer;
// create the new composer
this.composer = new ChatterComposer(this, this.thread_dataset, {
internal_subtypes: this.options.internal_subtypes,
is_log: options && options.is_log,
context: this.context,
record_name: this.record_name,
});
this.composer.insertBefore(this.$('.o_mail_thread')).then(function () {
// destroy existing composer
if (old_composer) {
old_composer.destroy();
}
self.composer.on('post_message', self, self.on_post_message);
});
},
close_composer: function () {
if (this.composer.is_empty()) {
this.composer.do_hide();
}
},
destroy: function () {
chat_manager.remove_chatter_messages(this.model);
this._super.apply(this, arguments);
},
});
// -----------------------------------------------------------------------------
// Utils
// -----------------------------------------------------------------------------
/**
* Parses text to find email: Tagada <address@mail.fr> -> [Tagada, address@mail.fr] or False
*/
var parse_email = function (text) {
var result = text.match(/(.*)<(.*@.*)>/);
if (result) {
return [_.str.trim(result[1]), _.str.trim(result[2])];
}
result = text.match(/(.*@.*)/);
if (result) {
return [_.str.trim(result[1]), _.str.trim(result[1])];
}
return [text, false];
};
/**
* Replaces textarea text into html text (add <p>, <a>)
* TDE note : should be done server-side, in Python -> use mail.compose.message ?
*/
var get_text2html = function (text) {
return text
.replace(/((?:https?|ftp):\/\/[\S]+)/g,'<a href="$1">$1</a> ')
.replace(/[\n\r]/g,'<br/>');
};
core.form_widget_registry.add('mail_followers', Followers);
core.form_widget_registry.add('mail_thread', Chatter);
return Chatter;
});
+476
View File
@@ -0,0 +1,476 @@
odoo.define('mail.chat_client_action', function (require) {
"use strict";
var chat_manager = require('mail.chat_manager');
var ChatComposer = require('mail.ChatComposer');
var ChatThread = require('mail.ChatThread');
var ControlPanelMixin = require('web.ControlPanelMixin');
var core = require('web.core');
var data = require('web.data');
var Dialog = require('web.Dialog');
var framework = require('web.framework');
var Model = require('web.Model');
var pyeval = require('web.pyeval');
var SearchView = require('web.SearchView');
var Sidebar = require('web.Sidebar');
var Widget = require('web.Widget');
var QWeb = core.qweb;
var _t = core._t;
/**
* Widget : Invite People to Channel Dialog
*
* Popup containing a 'many2many_tags' custom input to select multiple partners.
* Search user according to the input, and trigger event when selection is validated.
**/
var PartnerInviteDialog = Dialog.extend({
dialog_title: _t('Invite people'),
template: "mail.PartnerInviteDialog",
init: function(parent, title, channel_id){
this.channel_id = channel_id;
this._super(parent, {
title: title,
size: "medium",
buttons: [{
text: _t("Invite"),
close: true,
classes: "btn-primary",
click: _.bind(this.on_click_add, this),
}],
});
this.PartnersModel = new Model('res.partner');
},
start: function(){
var self = this;
this.$input = this.$('.o_mail_chat_partner_invite_input');
this.$input.select2({
width: '100%',
allowClear: true,
multiple: true,
formatResult: function(item){
var css_class = "fa-circle" + (item.im_status === 'online' ? "" : "-o");
return $('<span class="fa">').addClass(css_class).text(item.text);
},
query: function (query) {
self.PartnersModel.call('im_search', [query.term, 20]).then(function(result){
var data = [];
_.each(result, function(partner){
partner.text = partner.name;
data.push(partner);
});
query.callback({results: data});
});
}
});
return this._super.apply(this, arguments);
},
on_click_add: function(){
var self = this;
var data = this.$input.select2('data');
if(data.length >= 1){
var ChannelModel = new Model('mail.channel');
return ChannelModel.call('channel_invite', [], {ids : [this.channel_id], partner_ids: _.pluck(data, 'id')})
.then(function(){
var names = _.pluck(data, 'text').join(', ');
var notification = _.str.sprintf(_t('You added <b>%s</b> to the conversation.'), names);
self.do_notify(_t('New people'), notification);
});
}
},
});
var ChatAction = Widget.extend(ControlPanelMixin, {
template: 'mail.client_action',
events: {
"click .o_mail_chat_channel_item": function (event) {
event.preventDefault();
var channel_id = this.$(event.currentTarget).data('channel-id');
this.set_channel(channel_id);
},
"click .o_mail_sidebar_title .o_add": function (event) {
event.preventDefault();
var type = $(event.target).data("type");
this.$('.o_mail_add_channel[data-type=' + type + ']')
.show()
.find("input").focus();
},
"blur .o_mail_add_channel input": function () {
this.$('.o_mail_add_channel')
.hide();
},
"click .o_mail_partner_unpin": function (event) {
event.stopPropagation();
var self = this;
var channel_id = $(event.target).data("channel-id");
chat_manager.unsubscribe(channel_id).then(function () {
self.render_sidebar();
self.set_channel("channel_inbox");
});
},
},
on_attach_callback: function () {
this.thread.scroll_to({offset: this.channels_scrolltop[this.channel_id]});
},
on_detach_callback: function () {
this.channels_scrolltop[this.channel_id] = this.thread.get_scrolltop();
},
init: function(parent, action) {
this._super.apply(this, arguments);
this.action_manager = parent;
this.domain = [];
this.action = action;
this.channels_scrolltop = {};
},
start: function() {
var self = this;
// create searchview
var options = {
$buttons: $("<div>"),
action: this.action,
disable_groupby: true,
};
var dataset = new data.DataSetSearch(this, 'mail.message');
var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false;
var default_channel = this.action.context.active_id || this.action.params.default_active_id || 'channel_inbox';
this.searchview = new SearchView(this, dataset, view_id, {}, options);
this.searchview.on('search_data', this, this.on_search);
this.sidebar = new Sidebar(this, {
sections: [
{name: 'action', label: _t('Action')},
],
items: {
action: [
{label: _t('Unsubscribe'), classname: 'o_mail_chat_button_unsubscribe', callback: this.on_click_button_unsubscribe},
{label: _t('Settings'), classname: 'o_mail_chat_button_settings', callback: this.on_click_button_settings},
],
},
});
this.composer = new ChatComposer(this);
this.thread = new ChatThread(this, {
no_content_helper: this.action.help,
});
this.$buttons = $(QWeb.render("mail.chat.ControlButtons", {}));
this.$buttons.find('button').css({display:"inline-block"});
this.$buttons.on('click', '.o_mail_chat_button_invite', this.on_click_button_invite);
this.$buttons.on('click', '.o_mail_chat_button_detach', this.on_click_button_detach);
this.thread.on('redirect', this, this.on_redirect);
this.thread.on('load_more_messages', this, this.load_more_messages);
this.thread.on('mark_as_read', this, function (message_id) {
chat_manager.mark_as_read(message_id);
});
this.thread.on('toggle_star_status', this, function (message_id) {
chat_manager.toggle_star_status(message_id);
});
this.composer.on('post_message', this, this.on_post_message);
var def1 = this.thread.prependTo(this.$('.o_mail_chat_content'));
var def2 = this.composer.appendTo(this.$('.o_mail_chat_content'));
var def3 = this.searchview.appendTo($("<div>"));
var def4 = this.sidebar.appendTo($("<div>"));
this.render_sidebar();
return $.when(def1, def2, def3, def4)
.then(this.set_channel.bind(this, default_channel))
.then(function () {
chat_manager.bus.on('new_message', self, self.on_new_message);
chat_manager.bus.on('update_message', self, self.on_update_message);
chat_manager.bus.on('new_channel', self, self.on_new_channel);
chat_manager.bus.on('anyone_listening', self, function (channel, query) {
query.is_displayed = query.is_displayed || channel.id === self.channel_id;
});
chat_manager.bus.on('update_needaction', self, self.on_update_needaction);
});
},
render_sidebar: function () {
var self = this;
var $sidebar = QWeb.render("mail.chat.Sidebar", {
channels: chat_manager.get_channels(),
needaction_counter: chat_manager.get_needaction_counter()
});
this.$(".o_mail_chat_sidebar").replaceWith($sidebar);
this.$('.o_mail_add_channel[data-type=public]').find("input").autocomplete({
source: function(request, response) {
self.last_search_val = request.term;
self.do_search_channel(request.term).done(function(result){
result.push({
'label': _.str.sprintf('<strong>'+_t("Create %s")+'</strong>', '<em>"'+self.last_search_val+'"</em>'),
'value': '_create',
});
response(result);
});
},
select: function(event, ui) {
if (self.last_search_val) {
if (ui.item.value === '_create') {
chat_manager.create_channel(self.last_search_val, "public");
} else {
chat_manager.join_channel(ui.item.id);
}
}
},
focus: function(event) {
event.preventDefault();
},
html: true,
});
this.$('.o_mail_add_channel[data-type=dm]').find("input").autocomplete({
source: function(request, response) {
self.last_search_val = request.term;
self.do_search_partner(request.term).done(function(result){
response(result);
});
},
select: function(event, ui) {
var partner_id = ui.item.id;
chat_manager.create_channel(partner_id, "dm");
},
focus: function(event) {
event.preventDefault();
},
html: true,
});
this.$('.o_mail_add_channel[data-type=private]').find("input").on('keyup', this, function (event) {
var name = $(event.target).val();
if(event.which === $.ui.keyCode.ENTER && name) {
chat_manager.create_channel(name, "private");
}
});
},
do_search_channel: function(search_val){
var Channel = new Model("mail.channel");
return Channel.call('channel_search_to_join', [search_val]).then(function(result){
var values = [];
_.each(result, function(channel){
values.push(_.extend(channel, {
'value': channel.name,
'label': channel.name,
}));
});
return values;
});
},
do_search_partner: function (search_val) {
var Partner = new Model("res.partner");
return Partner.call('im_search', [search_val, 20]).then(function(result){
var values = [];
_.each(result, function(user){
values.push(_.extend(user, {
'value': user.name,
'label': user.name,
}));
});
return values;
});
},
set_channel: function (channel_id) {
var self = this;
// Store scroll position of previous channel
if (this.channel_id) {
this.channels_scrolltop[this.channel_id] = this.thread.get_scrolltop();
}
var new_channel_scrolltop = this.channels_scrolltop[channel_id];
var channel = chat_manager.get_channel(channel_id);
channel_id = channel.id;
this.channel_id = channel_id;
this.set("title", channel.name);
this.$buttons.toggle(channel.type !== "static");
this.$buttons
.find('.o_mail_chat_button_invite')
.toggle(channel.type !== "dm");
this.update_cp();
this.action.context.active_id = channel_id;
this.action.context.active_ids = [channel_id];
this.$('.o_mail_chat_channel_item')
.removeClass('o_active')
.filter('[data-channel-id=' + channel_id + ']')
.removeClass('o_unread_message')
.addClass('o_active');
this.$('.o_chat_composer').toggle(channel.type !== 'static');
this.sidebar.$el.toggle(channel.type !== 'static');
return this.fetch_and_render_thread().then(function () {
self.thread.scroll_to({offset: new_channel_scrolltop});
self.action_manager.do_push_state({
action: self.action.id,
active_id: self.channel_id,
});
});
},
fetch_and_render_thread: function () {
var self = this;
return chat_manager.fetch(this.channel_id, this.domain).then(function(result) {
self.thread.render(result, {display_load_more: !chat_manager.all_history_loaded(self.channel_id)});
});
},
load_more_messages: function () {
var self = this;
var oldest_msg_id = this.$('.o_thread_message').first().data('messageId');
var oldest_msg_selector = '.o_thread_message[data-message-id="' + oldest_msg_id + '"]';
var offset = -framework.getPosition(document.querySelector(oldest_msg_selector)).top;
return chat_manager
.fetch_more(this.channel_id, this.domain)
.then(function(result) {
self.thread.render(result, {display_load_more: !chat_manager.all_history_loaded(self.channel_id)});
offset += framework.getPosition(document.querySelector(oldest_msg_selector)).top;
self.thread.scroll_to({offset: offset});
});
},
update_cp: function () {
this.update_control_panel({
breadcrumbs: this.action_manager.get_breadcrumbs(),
cp_content: {
$buttons: this.$buttons,
$searchview: this.searchview.$el,
$searchview_buttons: this.searchview.$buttons.contents(),
$sidebar: this.sidebar.$el,
},
searchview: this.searchview,
});
},
on_search: function (domains) {
var result = pyeval.sync_eval_domains_and_contexts({
domains: domains
});
this.domain = result.domain;
this.fetch_and_render_thread();
},
on_redirect: function (res_model, res_id) {
this.action_manager.do_push_state({
model: res_model,
id: res_id,
});
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,
});
},
on_reverse_breadcrumb: function () {
this.update_cp(); // do not reload the client action, just display it, but a refresh of the control panel is needed.
this.action_manager.do_push_state({
action: this.action.id,
active_id: this.channel_id,
});
},
on_post_message: function (message) {
message.channel_id = this.channel_id;
chat_manager
.post_message(message)
.fail(function () {
// todo: display notification
});
},
on_new_message: function (message) {
var self = this;
if (_.contains(message.channel_ids, this.channel_id)) {
this.fetch_and_render_thread().then(function () {
self.thread.scroll_to({id: message.id});
});
}
// Indicate in the sidebar that there is a new message in the corresponding channels
_.each(_.without(message.channel_ids, this.channel_id), function (channel_id) {
self.$('.o_mail_chat_channel_item')
.filter('[data-channel-id=' + channel_id + ']')
.addClass('o_unread_message');
});
// Dump scroll position of channels in which the new message arrived
this.channels_scrolltop = _.omit(this.channels_scrolltop, message.channel_ids);
},
on_update_message: function (message) {
var self = this;
if ((this.channel_id === "channel_starred" && !message.is_starred) ||
(this.channel_id === "channel_inbox" && !message.is_needaction)) {
chat_manager.fetch(this.channel_id, this.domain).then(function (messages) {
var options = {display_load_more: !chat_manager.all_history_loaded(self.channel_id)};
self.thread.remove_message_and_render(message.id, messages, options);
});
} else if (_.contains(message.channel_ids, this.channel_id)) {
this.fetch_and_render_thread();
}
},
on_new_channel: function (channel) {
this.render_sidebar();
if (channel.autoswitch) {
this.set_channel(channel.id);
}
},
on_update_needaction: function () {
var $needaction_badge = QWeb.render("mail.chat.SidebarNeedaction", {
needaction_counter: chat_manager.get_needaction_counter()
});
this.$('.o_mail_sidebar_needaction').replaceWith($needaction_badge);
},
on_click_button_invite: function () {
var channel = chat_manager.get_channel(this.channel_id);
var title = _.str.sprintf(_t('Invite people to %s'), channel.name);
new PartnerInviteDialog(this, title, this.channel_id).open();
},
on_click_button_detach: function () {
chat_manager.detach_channel(this.channel_id);
},
on_click_button_unsubscribe: function () {
var self = this;
var channel = chat_manager.get_channel(this.channel_id);
chat_manager
.unsubscribe(channel.id)
.then(this.set_channel.bind(this, "channel_inbox"))
.then(this.render_sidebar.bind(this))
.then(function () {
var msg = _.str.sprintf(_t('You unsubscribed from <b>%s</b>.'), channel.name);
self.do_notify(_t("Unsubscribed"), msg);
});
},
on_click_button_settings: function() {
this.do_action({
type: 'ir.actions.act_window',
res_model: "mail.channel",
res_id: this.channel_id,
views: [[false, 'form']],
target: 'current'
}, {
on_reverse_breadcrumb: this.on_reverse_breadcrumb,
});
},
});
core.action_registry.add('mail.chat.instant_messaging', ChatAction);
});
+486
View File
@@ -0,0 +1,486 @@
odoo.define('mail.ChatComposer', function (require) {
"use strict";
var chat_manager = require('mail.chat_manager');
var core = require('web.core');
var data = require('web.data');
var Model = require('web.Model');
var session = require('web.session');
var Widget = require('web.Widget');
var QWeb = core.qweb;
var _t = core._t;
var Composer = Widget.extend({
template: "mail.ChatComposer",
events: {
"keydown .o_composer_input": "on_keydown",
"keyup .o_composer_input": "on_keyup",
"change input.o_form_input_file": "on_attachment_change",
"click .o_composer_button_send": "send_message",
"click .o_composer_button_add_attachment": "on_click_add_attachment",
"click .o_attachment_delete": "on_attachment_delete",
"click .o_mention_proposition": "on_click_mention_item",
"mouseover .o_mention_proposition": "on_hover_mention_proposition",
},
init: function (parent, options) {
this._super.apply(this, arguments);
this.options = _.defaults(options || {}, {
context: {},
mention_delimiter: '@',
mention_min_length: 2,
mention_typing_speed: 400,
mention_fetch_limit: 8,
});
this.context = this.options.context;
// Attachments
this.AttachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context);
this.fileupload_id = _.uniqueId('o_chat_fileupload');
this.set('attachment_ids', []);
// Mention
this.PartnerModel = new Model('res.partner');
this.set('mention_partners', []); // proposition of not-mention partner matching the mention_word
this.set('mention_selected_partners', []); // contains the mention partners sorted as they appear in the input text
},
start: function () {
var self = this;
this.$attachment_button = this.$(".o_composer_button_add_attachment");
this.$attachments_list = this.$('.o_composer_attachments_list');
this.$mention_partner_tags = this.$('.o_composer_mentioned_partners');
this.$mention_dropdown = this.$('.o_composer_mention_dropdown');
this.$input = this.$('.o_composer_input');
this.$input.focus();
// Attachments
$(window).on(this.fileupload_id, this.on_attachment_loaded);
this.on("change:attachment_ids", this, this.render_attachments);
// Emoji
this.$('.o_composer_button_emoji').popover({
placement: 'top',
content: function() {
if (!self.$emojis) { // lazy rendering
self.$emojis = $(QWeb.render('mail.ChatComposer.emojis', {
emojis: chat_manager.get_emojis(),
}));
self.$emojis.filter('.o_mail_emoji').on('click', self, self.on_click_emoji_img);
}
return self.$emojis;
},
html: true,
container: '.o_composer_emoji',
trigger: 'focus',
});
// Mention
this.on('change:mention_partners', this, this.render_mention_partners);
this.on('change:mention_selected_partners', this, this.render_mention_selected_partners);
return this._super();
},
preprocess_message: function () {
// Return a deferred as this function is extended with asynchronous
// behavior for the chatter composer
return $.when({
content: this.mention_preprocess_message(this.$input.val()),
attachment_ids: _.pluck(this.get('attachment_ids'), 'id'),
partner_ids: _.pluck(this.get('mention_selected_partners'), 'id'),
});
},
send_message: function () {
if (this.is_empty() || !this.do_check_attachment_upload()) {
return;
}
var self = this;
this.preprocess_message().then(function (message) {
self.trigger('post_message', message);
// Empty input, selected partners and attachments
self.$input.val('');
self.set('mention_selected_partners', []);
self.set('attachment_ids', []);
self.$input.focus();
});
},
// Events
on_click_add_attachment: function () {
this.$('input.o_form_input_file').click();
this.$input.focus();
},
on_click_emoji_img: function(event) {
this.$input.val(this.$input.val() + " " + $(event.currentTarget).data('emoji') + " ");
this.$input.focus();
},
on_keydown: function (event) {
switch(event.which) {
// UP, DOWN: prevent moving cursor if navigation in mention propositions
case $.ui.keyCode.UP:
case $.ui.keyCode.DOWN:
if (this.get('mention_partners').length) {
event.preventDefault();
}
break;
// BACKSPACE, DELETE: check if need to remove a mention
case $.ui.keyCode.BACKSPACE:
case $.ui.keyCode.DELETE:
this.mention_check_remove();
break;
// ENTER: submit the message only if the dropdown mention proposition is not displayed
case $.ui.keyCode.ENTER:
event.preventDefault();
if (!this.get('mention_partners').length) {
this.send_message();
}
break;
}
},
on_keyup: function (event) {
switch(event.which) {
// ESCAPED KEYS: do nothing
case $.ui.keyCode.END:
case $.ui.keyCode.PAGE_UP:
case $.ui.keyCode.PAGE_DOWN:
break;
// ESCAPE: close mention propositions
case $.ui.keyCode.ESCAPE:
this.set('mention_partners', []);
break;
// ENTER, UP, DOWN: check if navigation in mention propositions
case $.ui.keyCode.ENTER:
case $.ui.keyCode.UP:
case $.ui.keyCode.DOWN:
this.mention_proposition_navigation(event.which);
break;
// Otherwise, check if a mention is typed
default:
this.mention_word = this.mention_detect_delimiter();
if (this.mention_word) {
this.mention_word_changed();
} else {
this.set('mention_partners', []); // close the dropdown
}
}
},
// Attachments
on_attachment_change: function(event) {
var $target = $(event.target);
if ($target.val() !== '') {
var filename = $target.val().replace(/.*[\\\/]/,'');
// if the files exits for this answer, delete the file before upload
var attachments = [];
for (var i in this.get('attachment_ids')) {
if ((this.get('attachment_ids')[i].filename || this.get('attachment_ids')[i].name) === filename) {
if (this.get('attachment_ids')[i].upload) {
return false;
}
this.AttachmentDataSet.unlink([this.get('attachment_ids')[i].id]);
} else {
attachments.push(this.get('attachment_ids')[i]);
}
}
// submit filename
this.$('form.o_form_binary_form').submit();
this.$attachment_button.prop('disabled', true);
attachments.push({
'id': 0,
'name': filename,
'filename': filename,
'url': '',
'upload': true,
'mimetype': '',
});
this.set('attachment_ids', attachments);
}
},
on_attachment_loaded: function(event, result) {
var attachment_ids = [];
if (result.error || !result.id ) {
this.do_warn(result.error);
attachment_ids = _.filter(this.get('attachment_ids'), function (val) { return !val.upload; });
} else {
_.each(this.get('attachment_ids'), function(a) {
if (a.filename === result.filename && a.upload) {
attachment_ids.push({
'id': result.id,
'name': result.name || result.filename,
'filename': result.filename,
'mimetype': result.mimetype,
'url': session.url('/web/content', {'id': result.id, download: true}),
});
} else {
attachment_ids.push(a);
}
});
}
this.set('attachment_ids', attachment_ids);
this.$attachment_button.prop('disabled', false);
},
on_attachment_delete: function(event){
event.stopPropagation();
var self = this;
var attachment_id = $(event.target).data("id");
if (attachment_id) {
var attachments = [];
_.each(this.get('attachment_ids'), function(attachment){
if (attachment_id !== attachment.id) {
attachments.push(attachment);
} else {
self.AttachmentDataSet.unlink([attachment_id]);
}
});
this.set('attachment_ids', attachments);
}
},
do_check_attachment_upload: function () {
if (_.find(this.get('attachment_ids'), function (file) { return file.upload; })) {
this.do_warn(_t("Uploading error"), _t("Please, wait while the file is uploading."));
return false;
}
return true;
},
render_attachments: function() {
this.$attachments_list.html(QWeb.render('mail.ChatComposer.Attachments', {
attachments: this.get('attachment_ids'),
}));
},
// Mention
on_click_mention_item: function (event) {
event.preventDefault();
var text_input = this.$input.val();
var partner_id = $(event.currentTarget).data('partner-id');
var selected_partner = _.filter(this.get('mention_partners'), function (p) {
return p.id === partner_id;
})[0];
// add the mention partner to the list
var mention_selected_partners = this.get('mention_selected_partners');
if (mention_selected_partners.length) { // there are already mention partners
// get mention matches (ordered by index in the text)
var matches = this.mention_get_match(text_input);
var index = this.mention_get_index(matches, this.get_selection_positions().start);
mention_selected_partners.splice(index, 0, selected_partner);
mention_selected_partners = _.clone(mention_selected_partners);
} else { // this is the first mentionned partner
mention_selected_partners = mention_selected_partners.concat([selected_partner]);
}
this.set('mention_selected_partners', mention_selected_partners);
// update input text, and reset dropdown
var cursor_position = this.get_selection_positions().start;
var text_left = text_input.substring(0, cursor_position-(this.mention_word.length+1));
var text_right = text_input.substring(cursor_position, text_input.length);
var text_input_new = text_left + this.options.mention_delimiter + selected_partner.name + ' ' + text_right;
this.$input.val(text_input_new);
this.set_cursor_position(text_left.length+selected_partner.name.length+2);
this.set('mention_partners', []);
},
on_hover_mention_proposition: function (event) {
var $elem = $(event.currentTarget);
this.$('.o_mention_proposition').removeClass('active');
$elem.addClass('active');
},
mention_proposition_navigation: function (keycode) {
var $active = this.$('.o_mention_proposition.active');
if (keycode === $.ui.keyCode.ENTER) { // selecting proposition
$active.click();
} else { // navigation in propositions
var $to;
if (keycode === $.ui.keyCode.DOWN) {
$to = $active.next('.o_mention_proposition:not(.active)');
} else {
$to = $active.prev('.o_mention_proposition:not(.active)');
}
if ($to.length) {
$active.removeClass('active');
$to.addClass('active');
}
}
},
/**
* Return the text attached to the mention delimiter
* @returns {String|false}: the text right after the delimiter or false
*/
mention_detect_delimiter: function () {
var options = this.options;
var delimiter = options.mention_delimiter;
var text_val = this.$input.val();
var cursor_position = this.get_selection_positions().start;
var left_string = text_val.substring(0, cursor_position);
var search_str = text_val.substring(left_string.lastIndexOf(delimiter) - 1, cursor_position);
return validate_keyword(search_str);
function validate_keyword (search_str) {
var pattern = "(^"+delimiter+"|(^\\s"+delimiter+"))";
var regex_start = new RegExp(pattern, "g");
search_str = search_str.replace(/^\s\s*|^[\n\r]/g, '');
if (regex_start.test(search_str) && search_str.length > options.mention_min_length) {
search_str = search_str.replace(pattern, '');
return search_str.indexOf(' ') < 0 && !/[\r\n]/.test(search_str) ? search_str.replace(delimiter, '') : false;
}
return false;
}
},
mention_word_changed: function () {
var self = this;
// start a timeout to fetch partner with the current 'mention word'. The timer avoid to start
// an RPC for each pushed key when the user is typing the partner name.
// The 'mention_typing_speed' option should approach the time for a human to type a letter.
clearTimeout(this.mention_fetch_timer);
this.mention_fetch_timer = setTimeout(function () {
self.mention_fetch_partner(self.mention_word);
}, this.options.mention_typing_speed);
},
mention_fetch_partner: function (search_str) {
var self = this;
this.PartnerModel
.query(['id', 'name', 'email'])
.filter([['id', 'not in', _.pluck(this.get('mention_selected_partners'), 'id')],
'|', ['name', 'ilike', search_str], ['email', 'ilike', search_str]])
.limit(this.options.mention_fetch_limit)
.all().then(function (partners) {
self.set('mention_partners', partners);
});
},
mention_check_remove: function () {
var mention_selected_partners = this.get('mention_selected_partners');
var partners_to_remove = [];
var selection = this.get_selection_positions();
var deleted_binf = selection.start;
var deleted_bsup = selection.end;
var matches = this.mention_get_match(this.$input.val());
for (var i=0; i<matches.length; i++) {
var m = matches[i];
var m1 = m.index;
var m2 = m.index + m[0].length;
if (deleted_binf <= m2 && m1 < deleted_bsup) {
partners_to_remove.push(mention_selected_partners[i]);
}
}
this.set('mention_selected_partners', _.difference(mention_selected_partners, partners_to_remove));
},
mention_preprocess_message: function (message) {
var partners = this.get('mention_selected_partners');
if (partners.length) {
var matches = this.mention_get_match(message);
var substrings = [];
var start_index = 0;
for (var i=0; i<matches.length; i++) {
var match = matches[i];
var end_index = match.index + match[0].length;
var partner_name = match[0].substring(1);
var processed_text = _.str.sprintf("<a href='#' class='o_mail_redirect' data-oe-model='res.partner' data-oe-id='%s'>%s</a>", partners[i].id, partner_name);
var subtext = message.substring(start_index, end_index).replace(match[0], processed_text);
substrings.push(subtext);
start_index = end_index;
}
substrings.push(message.substring(start_index, message.length));
return substrings.join('');
}
return message;
},
render_mention_partners: function () {
if (this.get('mention_partners').length) {
this.$mention_dropdown.html(QWeb.render('mail.ChatComposer.MentionMenu', {
partners: this.get('mention_partners'),
}));
this.$mention_dropdown.addClass('open');
} else {
this.$mention_dropdown.removeClass('open');
this.$mention_dropdown.empty();
}
},
render_mention_selected_partners: function () {
this.$mention_partner_tags.html(QWeb.render('mail.ChatComposer.MentionTags', {
partners: this.get('mention_selected_partners'),
}));
},
/**
* Return the matches (as RexExp.exec does) for the partner mention in the input text
* @param {String} input_text: the text to search matches
* @returns {Object[]} matches in the same format as RexExp.exec()
*/
mention_get_match: function (input_text) {
var self = this;
// create the regex of all mention partner name
var partner_names = _.pluck(this.get('mention_selected_partners'), 'name');
var escaped_partner_names = _.map(partner_names, function (str) {
return "("+_.str.escapeRegExp(self.options.mention_delimiter+str)+")";
});
var regex_str = escaped_partner_names.join('|');
// extract matches
var result = [];
if(regex_str.length){
var myRegexp = new RegExp(regex_str, 'g');
var match = myRegexp.exec(input_text);
while (match !== null) {
result.push(match);
match = myRegexp.exec(input_text);
}
}
return result;
},
mention_get_index: function (matches, cursor_position) {
for (var i=0; i<matches.length; i++) {
if (cursor_position <= matches[i].index) {
return i;
}
}
return i;
},
// Others
get_selection_positions: function () {
var el = this.$input.get(0);
return el ? {start: el.selectionStart, end: el.selectionEnd} : {start: 0, end: 0};
},
set_cursor_position: function (pos) {
this.$input.each(function (index, elem) {
if (elem.setSelectionRange){
elem.setSelectionRange(pos, pos);
}
else if (elem.createTextRange){
elem.createTextRange().collapse(true).moveEnd('character', pos).moveStart('character', pos).select();
}
});
},
is_empty: function () {
return !this.$input.val().trim() && !this.$('.o_attachments').children().length;
},
});
return Composer;
});
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,
};
});
-459
View File
@@ -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,
};
});
-424
View File
@@ -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);
});
-823
View File
@@ -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,
};
});
-134
View File
@@ -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,
};
});
+81
View File
@@ -0,0 +1,81 @@
odoo.define('mail.systray', function (require) {
"use strict";
var core = require('web.core');
var Model = require('web.Model');
var SystrayMenu = require('web.SystrayMenu');
var web_client = require('web.web_client');
var Widget = require('web.Widget');
var chat_manager = require('mail.chat_manager');
/**
* Widget Top Menu Notification Counter
*
* Counter of notification in the Systray Menu. Need to know if InstantMessagingView is displayed to
* increment (or not) the counter. On click, should redirect to the client action.
**/
var NotificationTopButton = Widget.extend({
template:'mail.chat.NotificationTopButton',
events: {
"click": "on_click",
},
willStart: function () {
var self = this;
var ir_model = new Model("ir.model.data");
var def1 = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_menu_root_chat"]);
var def2 = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_action_client_chat"]);
return $.when(def1, def2, this._super()).then(function (menu_id, action_id) {
self.discuss_menu_id = menu_id;
self.client_action_id = action_id;
});
},
start: function () {
chat_manager.bus.on("update_needaction", this, this.update_counter);
this.update_counter(chat_manager.get_needaction_counter());
return this._super();
},
update_counter: function (counter) {
this.$('.fa-comment').html(counter);
},
on_click: function (event) {
var self = this;
event.preventDefault();
this.do_action(this.client_action_id, {clear_breadcrumbs: true}).then(function () {
core.bus.trigger('change_menu_section', self.discuss_menu_id);
});
},
});
SystrayMenu.Items.push(NotificationTopButton);
/**
* * 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);
});
+127
View File
@@ -0,0 +1,127 @@
odoo.define('mail.ChatThread', function (require) {
"use strict";
var core = require('web.core');
var Widget = require('web.Widget');
var QWeb = core.qweb;
var _t = core._t;
var ORDER = {
ASC: 1,
DESC: -1,
};
var Thread = Widget.extend({
className: 'o_mail_thread',
events: {
"click .o_mail_redirect": "on_click_redirect",
"click .o_thread_show_more": "on_click_show_more",
"click .o_thread_message_needaction": function (event) {
var message_id = $(event.currentTarget).data('message-id');
this.trigger("mark_as_read", message_id);
},
"click .o_thread_message_star": function (event) {
var message_id = $(event.currentTarget).data('message-id');
this.trigger("toggle_star_status", message_id);
},
},
init: function (parent, options) {
this._super.apply(this, arguments);
this.options = _.defaults(options || {}, {
display_order: ORDER.ASC,
display_needactions: true,
default_username: _t('Anonymous'),
display_document_link: true,
display_avatar: true,
});
},
render: function (messages, options) {
var msgs = _.map(messages, this._preprocess_message.bind(this));
if (this.options.display_order === ORDER.DESC) {
msgs.reverse();
}
this.$el.html(QWeb.render('mail.ChatThread', {
messages: msgs,
options: _.extend({}, this.options, options),
ORDER: ORDER,
}));
},
on_click_redirect: function (event) {
event.preventDefault();
var res_id = $(event.target).data('oe-id');
var res_model = $(event.target).data('oe-model');
this.trigger('redirect', res_model, res_id);
},
on_click_show_more: function () {
this.trigger('load_more_messages');
},
_preprocess_message: function (message) {
var msg = _.extend({}, message);
// Compute displayed author
if (!msg.author_id || !msg.author_id[1]) {
msg.displayed_author = msg.email_from || this.options.default_username;
}
// Compute the avatar_url
if (msg.author_id && msg.author_id[0]) {
msg.avatar_src = "/web/image/res.partner/" + msg.author_id[0] + "/image_small";
} else if (msg.msg_type === 'email') {
msg.avatar_src = "/mail/static/src/img/email_icon.png";
} else {
msg.avatar_src = "/mail/static/src/img/smiley/avatar.jpg";
}
// Compute url of attachments
_.each(msg.attachment_ids, function(a) {
a.url = '/web/content/' + a.id + '?download=true';
});
return msg;
},
/**
* Removes a message and re-renders the thread
* @param {int} [message_id] the id of the removed message
* @param {array} [messages] the list of messages to display, without the removed one
* @param {object} [options] options for the thread rendering
*/
remove_message_and_render: function (message_id, messages, options) {
var self = this;
this.$('.o_thread_message[data-message-id=' + message_id + ']').fadeOut({
done: function () { self.render(messages, options); }
});
},
/**
* Scrolls the thread to a given message or offset if any, to bottom otherwise
* @param {int} [target.id] optional: the id of the message to scroll to
* @param {int} [target.offset] optional: the number of pixels to scroll
*/
scroll_to: function (target) {
target = target || {};
if (target.id !== undefined) {
this.$el.scrollTo(this.$('.o_thread_message[data-message-id=' + target.id + ']'));
} else if (target.offset !== undefined) {
this.$el.scrollTop(target.offset);
} else {
this.$el.scrollTop(this.el.scrollHeight);
}
},
get_scrolltop: function () {
return this.$el.scrollTop();
},
});
Thread.ORDER = ORDER;
return Thread;
});
+115
View File
@@ -0,0 +1,115 @@
odoo.define('mail.window_manager', function (require) {
"use strict";
var chat_manager = require('mail.chat_manager');
var ChatWindow = require('mail.ChatWindow');
var core = require('web.core');
var web_client = require('web.web_client');
// chat window management
//----------------------------------------------------------------
var chat_sessions = [];
function open_chat (session) {
if (!_.findWhere(chat_sessions, {id: session.id})) {
var chat_session = {
id: session.id,
uuid: session.uuid,
name: session.name,
window: new ChatWindow(web_client, session.id, session.name, session.is_folded),
};
chat_session.window.on("close_chat_session", null, function () {
close_chat(chat_session);
chat_manager.close_chat_session(chat_session.id);
});
chat_session.window.on("toggle_star_status", null, function (message_id) {
chat_manager.toggle_star_status(message_id);
});
chat_session.window.on("fold_channel", null, function (channel_id) {
chat_manager.fold_channel(channel_id);
});
chat_session.window.on("post_message", null, function (message) {
chat_manager.post_message(message);
});
chat_sessions.push(chat_session);
chat_session.window.appendTo($('body'))
.then(reposition_windows)
.then(function () {
return chat_manager.get_messages({channel_id: chat_session.id});
}).then(function (messages) {
chat_session.window.render(messages);
chat_session.window.scrollBottom();
});
}
}
function close_chat (chat_session) {
var session = _.find(chat_sessions, {id: chat_session.id});
if (session) {
chat_sessions = _.without(chat_sessions, session);
session.window.destroy();
reposition_windows();
}
}
function toggle_fold_chat (channel) {
var session = _.find(chat_sessions, {id: channel.id});
if (session) {
session.window.toggle_fold();
}
}
function reposition_windows () {
_.each(chat_sessions, function (session, index) {
session.window.$el.css({right: 245 * index, bottom: 0});
});
}
function update_sessions (message, scrollBottom) {
_.each(chat_sessions, function (session) {
if (_.contains(message.channel_ids, session.id)) {
chat_manager.get_messages({channel_id: session.id}).then(function (messages) {
session.window.render(messages);
if (scrollBottom) {
session.window.scrollBottom();
}
});
}
});
}
core.bus.on('web_client_ready', null, function () {
chat_manager.bus.on('open_chat', null, open_chat);
chat_manager.bus.on('close_chat', null, close_chat);
chat_manager.bus.on('channel_toggle_fold', null, toggle_fold_chat);
chat_manager.bus.on('new_message', null, function (message) {
update_sessions(message, true);
});
chat_manager.bus.on('update_message', null, function (message) {
update_sessions(message, false);
});
chat_manager.bus.on('anyone_listening', null, function (channel, query) {
_.each(chat_sessions, function (session) {
if (_.contains(channel.id, session.id)) {
query.is_displayed = true;
}
});
});
_.each(chat_manager.get_channels(), function (channel) {
if (channel.is_detached) {
open_chat(channel);
}
});
});
});
@@ -0,0 +1,50 @@
@o-chat-window-bg: lighten(lightgray, 13%);
.o_chat_window {
.o-flex-display();
.o-flex-flow(column, nowrap);
position: fixed;
width: 240px;
height: 333px;
background-color: @o-chat-window-bg;
border: 1px solid gray;
.o_chat_header {
.o-flex-display();
.o-flex(0, 0, auto);
color: white;
background-color: @odoo-brand-primary;
padding: 5px;
.o_chat_title {
cursor: pointer;
.o-flex(1, 1, auto);
}
.o_chat_window_close {
color: white;
padding: 3px;
&:hover {
background-color: darken(@odoo-brand-primary, 10%);
}
}
}
.o_chat_content {
.o-flex(1, 1, auto);
overflow: auto;
padding: 5px;
.o_mail_thread .o_thread_date_separator {
border-color: lightgray;
.o_thread_date {
background-color: @o-chat-window-bg;
}
}
}
.o_chat_input > input {
padding: 5px;
width: 100%;
}
}
@@ -1,14 +1,48 @@
.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
.o_chatter {
width: 100%;
.o_chat_composer {
margin-top: 10px;
.o_chatter_composer_info {
display: inline-block;
margin-bottom: 5px;
color: @odoo-main-color-muted;
font-style: italic;
}
.o_chatter_composer_subtypes {
> label {
display: inline-block; // Required for old design compatibility
width: 100px;
}
.o_chatter_composer_subtype_select {
display: inline-block;
min-width: 150px;
width: auto;
}
}
.o_composer {
margin-top: 10px;
.o_composer_input {
width: 100%;
margin-bottom: 5px;
}
}
}
.o_mail_thread {
margin-top: 10px;
}
}
.o_followers {
float: right;
@media (max-width: @screen-xs-max) {
height: auto; // to be displayed above the mail composer buttons
float: left;
display: none; // Hide the followers in mobile
}
.o_followers_actions {
@@ -16,7 +50,7 @@
float: right;
.o_followers_follow_button {
width: 110px;
width: 120px;
.o_follow, .o_followers_unfollow, .o_followers_following {
display: none;
@@ -49,7 +83,7 @@
float: right;
margin-left: 5px;
> button {
width: 120px;
width: 140px;
}
.o_followers_count {
@@ -80,7 +114,7 @@
}
.o_edit_subtype {
.o-position-absolute(6px, 18px);
cursor: pointer;
cursor: pointer;
}
}
@@ -0,0 +1,190 @@
// ------------------------------------------------------------------
// 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, 200px);
overflow: auto;
padding: 8px 0;
.o_mail_sidebar_title {
.o-webclient-padding(@top: 5px, @bottom: 5px);
.o-flex-display();
align-items: center;
justify-content: space-between;
h4 {
text-transform: uppercase;
}
.o_add {
padding: 8px;
}
}
.o_mail_chat_channel_item {
width: 100%;
.o-webclient-padding(@top: 5px, @bottom: 5px);
padding-left: 1.5*@odoo-horizontal-padding; // larger padding than titles
> .badge {
margin: 0;
}
.o_mail_partner_unpin {
padding: 2px 8px;
}
&.o_unread_message {
font-weight: bold;
}
}
.o_mail_add_channel {
width: 100%;
.o-webclient-padding(@top: 5px, @bottom: 5px);
padding-left: 1.5*@odoo-horizontal-padding; // larger padding than titles
span {
margin-right: 5px;
}
}
}
.o_mail_chat_content {
.o-flex(1, 1, 100%);
height: 100%;
.o-flex-display();
.o-flex-flow(column, nowrap);
@media (max-width: @screen-xs-max) {
height: auto;
}
.o_mail_thread {
.o-flex(1, 0, 0);
overflow: auto;
.o-webclient-padding();
.o_chat_help_message {
padding: 25px 0;
text-align: justify;
}
}
.o_chat_composer {
.o-flex(0, 0, auto);
.o-webclient-padding(@top: 10px, @bottom: 5px);
.o_composer {
.o-flex-display();
.o_composer_input {
.o-flex(1, 1, auto);
padding: 5px;
margin-right: 5px;
border: none;
}
.o_composer_buttons {
.o-flex(0, 0, auto);
}
}
}
}
}
// ------------------------------------------------------------------
// Style
// ------------------------------------------------------------------
.o_mail_chat {
.o_mail_chat_sidebar {
background-color: @gray;
color: @gray-lighter;
.o_mail_chat_channel_item {
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
&:hover {
background-color: @gray-dark;
}
> .badge {
color: white;
background-color: @odoo-brand-optional;
}
&.o_active {
background-color: @gray-dark;
}
}
.o_mail_add_channel {
display: none;
input {
color: black;
}
}
.o_mail_chat_channel_unread {
font-weight: bold;
}
.o_mail_sidebar_title {
h4 {
font-size: 13px;
text-transform: uppercase;
}
.o_add {
cursor: pointer;
&:hover {
background-color: @gray-dark;
}
}
}
// .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_chat_composer {
border-top: 1px solid @odoo-brand-optional;
background-color: @odoo-brand-optional;
.o_attachment {
.o_attachment_name, .o_attachment_delete {
color: white;
}
}
.o_composer_mentioned_partners {
color: white;
> .badge {
color: white;
border: 1px solid white;
background-color: transparent;
}
}
}
}
}
+97
View File
@@ -0,0 +1,97 @@
.o_composer_mentioned_partners, .o_composer_attachments_list {
margin-top: 5px;
text-align: justify;
}
// Emoji size in the popover
.o_mail_emoji {
display: inline-block;
height: 24px;
width: 24px;
> img {
cursor: pointer;
width: 100%;
}
}
// Attachment Icons (common for chat thread and chat composer)
// ------------------------------------------------------------------
@-webkit-keyframes oe_mail_attach_loading_anim {
0% { background: @odoo-brand-optional }
50% { background: darken(@odoo-brand-optional, 10%) }
100% { background: @odoo-brand-optional }
}
@-moz-keyframes oe_mail_attach_loading_anim {
0% { background: @odoo-brand-optional }
50% { background: darken(@odoo-brand-optional, 10%) }
100% { background: @odoo-brand-optional }
}
@-o-keyframes oe_mail_attach_loading_anim {
0% { background: @odoo-brand-optional }
50% { background: darken(@odoo-brand-optional, 10%) }
100% { background: @odoo-brand-optional }
}
@keyframes oe_mail_attach_loading_anim {
0% { background: @odoo-brand-optional }
50% { background: darken(@odoo-brand-optional, 10%) }
100% { background: @odoo-brand-optional }
}
@o-mail-attachment-image-size: 60px;
.o_attachment {
position: relative;
width: 2*@o-mail-attachment-image-size;
display: inline-block;
height: @o-mail-attachment-image-size;
text-align: center;
margin-bottom: 15px;
.o_image {
.o-position-absolute(@top: 0, @left: @o-mail-attachment-image-size/2);
width: @o-mail-attachment-image-size;
height: @o-mail-attachment-image-size;
}
.o_attachment_name {
display: inline-block;
max-width: 100%;
margin-top: @o-mail-attachment-image-size;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: @odoo-main-text-color;
}
.o_attachment_progress_bar {
display: none;
}
.o_attachment_delete {
.o-position-absolute(@top: 0, @right: 0);
color: @odoo-main-text-color;
cursor: pointer;
opacity: 0;
.o-transition(opacity, 0.2s, linear);
}
&.o_attachment_uploading {
.o_attachment_progress_bar {
display: block;
.o-position-absolute(@top: 18px, @left: 16px, @right: 16px);
height: 17px;
line-height: 13px;
color: white;
border-radius: @border-radius-base;
border: solid 1px grey;
box-shadow: 0px 1px 10px @odoo-shadow-color;
.o-animation(@name: oe_mail_attach_loading_anim, @duration: 1s, @timing-function: linear, @iteration-count: infinite);
}
.o_attachment_delete {
display: none;
}
}
&:hover .o_attachment_delete {
opacity: 1;
.o-transition(opacity, 0.2s, linear);
}
}
@@ -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,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);
}
}
+71
View File
@@ -0,0 +1,71 @@
@mail-thread-avatar-size: 50px;
.o_mail_thread {
.o_thread_show_more {
text-align: center;
}
.o_thread_date_separator {
margin-bottom: 15px;
border-bottom: solid @gray-lighter 2px;
text-align: center;
.o_thread_date {
position: relative;
top: 10px;
margin: 0 auto;
padding: 0 10px;
font-weight: bold;
background: white;
}
}
.o_thread_message {
.o-flex-display();
margin: 10px 0;
.o_thread_message_avatar {
.o-flex(0, 0, @mail-thread-avatar-size);
margin-right: 10px;
@media (max-width: @screen-xs-max) {
.o-flex(0, 0, @mail-thread-avatar-size/2);
}
.o_thread_message_avatar_img {
display: block;
margin: 0 auto; // center the avatar
max-width: @mail-thread-avatar-size;
@media (max-width: @screen-xs-max) {
max-width: @mail-thread-avatar-size/2;
}
}
}
.o_thread_message_core {
.o-flex(1, 1, auto);
.o_mail_subject {
font-style: italic;
}
> p {
margin: 0 0 9px; // Required by the old design to override a general rule on p's
}
.o_thread_message_star, .o_thread_message_needaction {
cursor: pointer;
opacity: 0.5;
&:hover {
opacity: 1;
}
&.fa-star {
color: gold;
}
}
.o_mail_redirect {
cursor: pointer;
}
}
}
}
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ChatWindow">
<div class="o_chat_window">
<div class="o_chat_header">
<span class="o_chat_title">
<t t-raw="widget.title"/>
</span>
<span>
<a href="#" class="o_chat_window_close fa fa-close"></a>
</span>
</div>
<div class="o_chat_content">
<!-- <t t-call="mail.chat.im.Conversation.messages"/> -->
</div>
<div class="o_chat_input">
<input t-att-placeholder="widget.placeholder"/>
</div>
</div>
</t>
</templates>
+165
View File
@@ -0,0 +1,165 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<!--
Chatter composer
-->
<t t-name="mail.chatter.ChatComposer" t-extend="mail.ChatComposer">
<!-- Insert information before the composer -->
<t t-jquery=".o_composer" t-operation="before">
<!-- Internal note composer -->
<t t-if="widget.options.is_log">
<span class="o_chatter_composer_info">
Log an internal note which will not be sent to followers, but which can be read
by users accessing this document.
</span>
<!-- Internal subtypes -->
<div t-att-class="'o_chatter_composer_subtypes ' +
(widget.options.internal_subtypes.length &lt;= 1 ? ' hide' : '')">
<label>Activity</label>
<select class="o_chatter_composer_subtype_select">
<t t-foreach="widget.options.internal_subtypes" t-as="subtype">
<option t-att-value="subtype['id']"><t t-raw="subtype['name']"/></option>
</t>
</select>
</div>
</t>
<!-- New message composer -->
<t t-if="!widget.options.is_log">
<!-- Text for followers -->
<span class="o_chatter_composer_info">
To: Followers of
<t t-if="widget.options.record_name">
<t t-raw="'&quot;' + widget.options.record_name + '&quot;'"/>
</t>
<t t-if="!widget.options.record_name">
this document
</t>
</span>
<!-- List of followers -->
<div class="o_composer_suggested_partners">
<t t-foreach='widget.suggested_partners' t-as='recipient'>
<div t-attf-title="Add as recipient and follower (reason: #{recipient.reason})">
<input type="checkbox"
t-att-checked="recipient.checked ? 'checked' : undefined"
t-att-data-fullname="recipient.full_name"/>
<t t-esc="recipient.name"/>
<t t-if="recipient.email_address">(<t t-esc="recipient.email_address"/>)</t>
<t t-if="!recipient.email_address">(no email address)</t>
</div>
</t>
</div>
</t>
</t>
<!-- Replace the input by a textarea -->
<t t-jquery=".o_composer_input" t-operation="replace">
<textarea class="o_composer_input" placeholder="Write something"/>
</t>
<!-- Add a button to open the full composer dialog -->
<t t-jquery=".o_composer_buttons" t-operation="append">
<button class="btn btn-sm btn-icon fa fa-pencil-square-o o_composer_button_full_composer" type="button"/>
</t>
</t>
<!--
Chatter main div
-->
<t t-name="mail.Chatter">
<div class="o_chatter">
<div class="o_chatter_topbar">
<button type="button" class="btn btn-sm btn-primary o_chatter_button_new_message" title="Send a message">
New message
</button>
<button t-if="widget.options.display_log_button" class="btn btn-sm btn-link o_chatter_button_log_note" title="Log a note. Followers will not be notified.">
Log an internal note
</button>
</div>
</div>
</t>
<!--
Followers: main buttons
Template used to display the followers, the actions and the subtypes in a record.
-->
<t t-name="mail.Followers">
<div class="o_followers">
<div t-attf-class="o_followers_title_box">
<button t-attf-class="btn btn-sm btn-default dropdown-toggle"
data-toggle="dropdown" aria-expanded="false">
<span class="o_followers_count"/>
<span class="caret"/>
</button>
<ul class="dropdown-menu o_followers_list" role="menu"></ul>
</div>
<div class="o_followers_actions">
<div t-attf-class="btn-group btn-group-sm o_subtypes_list">
<button class="btn btn-sm btn-primary o_followers_follow_button o_followers_notfollow">
<span class="o_follow">Follow</span>
<span class="fa fa-times o_followers_unfollow"/>
<span class="o_followers_unfollow"> Unfollow</span>
<span class="fa fa-check o_followers_following"/>
<span class="o_followers_following"> Following</span>
</button><button t-attf-class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<span class="caret"/>
</button>
<ul class="dropdown-menu" role="menu"/>
</div>
</div>
</div>
</t>
<!--
Followers: Partner
Template used to display a partner following the record
-->
<t t-name="mail.Followers.partner">
<li class='o_partner'>
<img t-att-src="record.avatar_url"/>
<a class="o_mail_redirect"
t-attf-href="#"
t-att-title="record.name"
t-att-data-oe-model="record.res_model"
t-att-data-oe-id="record.res_id"><t t-esc="record.name"/></a>
<i t-if="record.is_editable and (widget.records_length &gt; 1)"
class="fa fa-pencil o_edit_subtype hide" title="Edit subscription"
t-att-data-id="record.id"
t-att-data-oe-id="record.res_id"
t-att-data-oe-model="record.res_model"/>
<i t-if="widget.view_is_editable" class="fa fa-remove o_remove_follower"
title="Remove this follower"/>
</li>
</t>
<!--
Followers: 'Add followers' and 'Add channels' buttons
-->
<t t-name="mail.Followers.add_more">
<t t-if="widget.view_is_editable">
<li class="o_add_follower">
<a href="#"> Add Followers </a>
</li>
<li class="o_add_follower_channel">
<a href="#"> Add Channels </a>
</li>
<li t-if="widget.followers.length > 0" class="divider"/>
</t>
</t>
<!--
Followers: Subtype
Template used to display message subtypes of a follower subscription
-->
<t t-name="mail.Followers.subtype">
<li class="o_subtype">
<input type="checkbox" t-att-checked="record.followed"
t-att-id="'input_mail_followers_subtype_'+record.id+(dialog ? '_in_dialog': '')"
t-att-data-id="record.id" t-att-name="record.name" class="o_subtype_checkbox"/>
<span t-att-for="'input_mail_followers_subtype_'+record.id+(dialog ? '_in_dialog': '')">
<t t-raw="record.name"/>
</span>
</li>
</t>
</templates>
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<!--
ChatMailThread
Layout of the client action
param: widget: the widget (ChatMailThread)
-->
<t t-name="mail.client_action">
<div class="o_mail_chat">
<div class="o_mail_chat_sidebar"/>
<div class="o_mail_chat_content"/>
</div>
</t>
<div t-name="mail.chat.Sidebar" class="o_mail_chat_sidebar">
<div class="o_mail_chat_channel_item" data-channel-id="channel_inbox">
@ Inbox
<t t-call="mail.chat.SidebarNeedaction"/>
</div>
<div class="o_mail_chat_channel_item" data-channel-id="channel_starred">
<span class="fa fa-star-o"/> Starred
</div>
<div class="o_mail_sidebar_title">
<h4>Channels</h4>
<span class="fa fa-plus o_add" data-type="public"/>
</div>
<t t-foreach="channels" t-as="channel">
<t t-if="channel.type === 'public'">
<div class="o_mail_chat_channel_item" t-att-data-channel-id="channel.id">
<t t-esc="channel.name"/>
</div>
</t>
</t>
<div class="o_mail_add_channel" data-type="public">
<input type="text" placeholder="Add a channel"/>
</div>
<div class="o_mail_sidebar_title">
<h4>Direct Messages</h4>
<span class="fa fa-plus o_add" data-type="dm"/>
</div>
<t t-foreach="channels" t-as="channel">
<t t-if="channel.type === 'dm'">
<div class="o_mail_chat_channel_item" t-att-data-channel-id="channel.id">
<i t-att-class="'fa ' + (channel.status == 'online' ? 'fa-circle' : 'fa-circle-o')"/>
<t t-esc="channel.name"/>
<span class="fa fa-times pull-right o_mail_partner_unpin" t-att-title="_t('Close')" t-att-data-channel-id="channel.id"/>
</div>
</t>
</t>
<div class="o_mail_add_channel" data-type="dm">
<input type="text" placeholder="Open chat"/>
</div>
<div class="o_mail_sidebar_title">
<h4>Private Channels</h4>
<span class="fa fa-plus o_add" data-type="private"/>
</div>
<t t-foreach="channels" t-as="channel">
<t t-if="channel.type === 'private'">
<div class="o_mail_chat_channel_item" t-att-data-channel-id="channel.id">
<t t-esc="channel.name"/>
</div>
</t>
</t>
<div class="o_mail_add_channel" data-type="private">
<input type="text" placeholder="Add a channel"/>
</div>
<!-- <a href="#" class="o_mail_chat_add_more_text"><span class="fa fa-plus"/>Add</a>
<div class="o_mail_chat_add_more_search_bar" style="display: none;">
<input type="text"/>
</div> -->
<!-- <t t-call="mail.chat.ChatMailThread.channels">
<t t-set="channel_slot" t-value="'channel_private_group'"/>
</t> -->
</div>
<t t-name="mail.chat.SidebarNeedaction">
<span t-attf-class="o_mail_sidebar_needaction badge pull-right #{(!needaction_counter ? 'hide' : '')}">
<t t-esc="needaction_counter"/>
</span>
</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_detach" t-att-title='_t("Detach the conversation")'>Detach</button>
</div>
</t>
<div t-name="mail.PartnerInviteDialog">
<input type="text" class="o_mail_chat_partner_invite_input" id="mail_search_partners"/>
</div>
</templates>
+57
View File
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<div t-name="mail.ChatComposer" class="o_chat_composer">
<div class="o_composer">
<div class="dropup o_composer_mention_dropdown"/>
<input type="text" class="o_composer_input" placeholder="Write something"/>
<div class="btn-group o_composer_buttons">
<button class="btn btn-sm btn-primary o_composer_button_send" type="button">Send</button>
<button class="btn btn-sm btn-icon fa fa-smile-o o_composer_button_emoji" type="button" data-toggle="popover"/>
<button class="btn btn-sm btn-icon fa fa-paperclip o_composer_button_add_attachment" type="button"/>
</div>
</div>
<div class="o_composer_mentioned_partners"/>
<div class="o_composer_attachments_list"/>
<span class="hide">
<t t-call="HiddenInputFile">
<t t-set="fileupload_id" t-value="widget.fileupload_id"/>
<t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t>
<input type="hidden" name="model" value="mail.compose.message"/>
<input type="hidden" name="id" value="0"/>
<input type="hidden" name="session_id" t-att-value="widget.session.session_id"/>
</t>
</span>
<span class="o_composer_emoji"></span>
</div>
<t t-name="mail.ChatComposer.emojis">
<t t-foreach="emojis" t-as="emoji">
<span t-att-data-emoji="emoji.source" class="o_mail_emoji" t-att-title="emoji.description">
<t t-raw="emoji.substitution"/>
</span>
</t>
</t>
<t t-name="mail.ChatComposer.MentionMenu">
<ul class="dropdown-menu" aria-labelledby="dropdownMailMentionMenu">
<t t-foreach="partners" t-as="partner">
<li t-att-class="'o_mention_proposition ' + (partner_index === 0 ? 'active' : '')"
t-att-data-partner-id="partner.id">
<a href="#">
<t t-esc="partner.name"/> <t t-if="partner.email">(<t t-esc="partner.email"/>)</t>
</a>
</li>
</t>
</ul>
</t>
<t t-name="mail.ChatComposer.MentionTags">
<t t-if="partners.length">Mention(s): </t>
<t t-foreach="partners" t-as="partner">
<span class="badge"><t t-esc="partner.name"/></span>
</t>
</t>
</templates>
@@ -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') &gt; 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 &gt; 0 ? 'o_mail_chat_channel_unread' : ' ')" t-att-data-channel-id="channel.id" role="presentation">
<a t-att-href="_.str.sprintf('#active_id=%s&amp;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 &gt; 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' &amp;&amp; (!message.author_id || !message.author_id[0]) &amp;&amp; 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' &amp;&amp; (message.author_id &amp;&amp; message.author_id[0]) &amp;&amp; !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' &amp;&amp; (message.author_id &amp;&amp; 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' &amp;&amp; (!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 &amp;&amp; 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 &lt;= 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="'&quot;' + widget.options.record_name + '&quot;'"/>
</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&amp;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 &gt; 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>
-196
View File
@@ -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' &amp;&amp; (!message.author_id || !message.author_id[0]) &amp;&amp; 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' &amp;&amp; (message.author_id &amp;&amp; message.author_id[0]) &amp;&amp; !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' &amp;&amp; (message.author_id &amp;&amp; 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' &amp;&amp; (!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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; !message.author_id[1] &amp;&amp; message.email_from">
<strong><t t-esc="message.email_from"/></strong>
</t>
<t t-if="message.author_id &amp;&amp; !message.author_id[1] &amp;&amp; !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 &amp;&amp; (message.model != 'mail.channel') &amp;&amp; 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 &amp;&amp; message.tracking_value_ids.length &gt; 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 &amp;&amp; message.tracking_value_ids.length &gt; 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>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<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>
<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') || ''"/>
</a>
</li>
</t>
</templates>
+122
View File
@@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ChatThread">
<t t-if="messages.length">
<t t-if="options.display_load_more &amp;&amp; options.display_order == ORDER.ASC"
t-call="mail.ChatThread.LoadMore"/>
<t t-call="mail.ChatThread.Content"/>
<t t-if="options.display_load_more &amp;&amp; options.display_order == ORDER.DESC"
t-call="mail.ChatThread.LoadMore"/>
</t>
<t t-if="!messages.length &amp;&amp; options.no_content_helper">
<div class="o_chat_help_message">
<t t-raw="options.no_content_helper"/>
</div>
</t>
</t>
<t t-name="mail.ChatThread.Content">
<t t-set="current_day" t-value="0"/>
<t t-foreach="messages" t-as="message">
<div t-if="current_day != message.date.split(' ')[0]" class="o_thread_date_separator">
<span class="o_thread_date">
<t t-raw="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('LL')"/>
</span>
<t t-set="current_day" t-value="message.date.split(' ')[0]"/>
</div>
<t t-call="mail.ChatThread.Message"/>
</t>
</t>
<t t-name="mail.ChatThread.Message">
<div class="o_thread_message" t-att-data-message-id="message.id">
<div t-if="options.display_avatar" class="o_thread_message_avatar">
<img t-if="message.avatar_src" t-att-src="message.avatar_src" class="o_thread_message_avatar_img"/>
</div>
<div class="o_thread_message_core">
<span class="pull-right">
<i t-if="message.is_needaction &amp;&amp; options.display_needactions" class="fa fa-check o_thread_message_needaction" t-att-data-message-id="message.id" t-att-title="_t('Mark as Done')"/>
<i t-att-class="'fa fa-lg o_thread_message_star ' + (message.is_starred ? 'fa-star' : 'fa-star-o')" t-att-data-message-id="message.id" t-att-title="_t('Mark as Todo')"/>
</span>
<p class="text-muted">
<t t-if="message.is_note">
Note by
</t>
<t t-if="message.author_id &amp;&amp; message.author_id[1]">
<strong class="o_mail_redirect" data-oe-model="res.partner" t-att-data-oe-id="message.author_id[0]"><t t-esc="message.author_id[1]"/></strong>
</t>
<t t-if="!message.author_id || !message.author_id[1]">
<strong class="o_mail_redirect"><t t-esc="message.displayed_author"/></strong>
</t>
<small t-att-title="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('LLL')">
- <t t-esc="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('hh:mm')"/>
</small>
<t t-if="message.model &amp;&amp; (message.model != 'mail.channel') &amp;&amp; options.display_document_link">
on <a class="o_mail_redirect" href="#" t-att-data-oe-model="message.model" t-att-data-oe-id="message.res_id"><t t-esc="message.record_name"/></a>
</t>
</p>
<t t-if="message.tracking_value_ids &amp;&amp; message.tracking_value_ids.length &gt; 0">
<t t-call="mail.ChatThread.MessageTracking"/>
</t>
<div t-if="message.subject" class="o_mail_subject">Subject: <t t-esc="message.subject"/></div>
<t t-if="!(message.tracking_value_ids &amp;&amp; message.tracking_value_ids.length &gt; 0)">
<t t-raw="message.body"/>
</t>
<t t-foreach="message.attachment_ids" t-as="attachment">
<t t-call="mail.Attachment"/>
</t>
</div>
</div>
</t>
<t t-name="mail.ChatThread.MessageTracking">
<ul class="o_mail_thread_message_tracking">
<t t-foreach='message.tracking_value_ids' t-as='value'>
<li>
<t t-esc="value.changed_field"/>:
<t t-if="value.old_value">
<span> <t t-esc="value.old_value"/> </span>
<span class="fa fa-long-arrow-right"/>
</t>
<span> <t t-esc="value.new_value"/> </span>
</li>
</t>
</ul>
</t>
<t t-name="mail.ChatComposer.Attachments">
<div class="o_attachments">
<t t-foreach="attachments" t-as='attachment'>
<t t-call="mail.Attachment">
<t t-set="editable" t-value="true"/>
</t>
</t>
</div>
</t>
<t t-name="mail.Attachment">
<div t-attf-class="o_attachment #{attachment.upload ? 'o_attachment_uploading' : ''}" t-att-title="attachment.name">
<a class="o_image" t-att-href='attachment.url' target="_blank" t-att-data-mimetype="attachment.mimetype" t-attf-data-src="/web/image/#{attachment.id}/100x80">
<span class='o_attachment_name'><t t-raw='attachment.name'/></span>
</a>
<t t-if="editable">
<div class="o_attachment_delete">
<i class='fa fa-times-circle' title="Delete this attachment" t-att-data-id="attachment.id"/>
</div>
<div class="o_attachment_progress_bar">
Uploading
</div>
</t>
</div>
</t>
<t t-name="mail.ChatThread.LoadMore">
<div t-if="!all_messages_loaded" class="o_thread_show_more">
<button class="btn btn-sm btn-link">-------- Show more messages --------</button>
</div>
</t>
</templates>
+14 -14
View File
@@ -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>
+2 -2
View File
@@ -1,9 +1,9 @@
odoo.define('mail_tip.mail_tip', function (require) {
"use strict";
var mail = require('mail.chatter');
var Chatter = require('mail.Chatter');
mail.ChatterMailThread.include({
Chatter.include({
render_value: function() {
var self = this;
this._super.apply(this, arguments).done(function() {
+1 -1
View File
@@ -262,7 +262,7 @@
log_when_loaded();
}
});
}, 100);
}, 1000);
};
$(log_when_loaded);
+3 -1
View File
@@ -15,7 +15,9 @@ function genericJsonRpc (fct_name, params, fct) {
var xhr = fct(data);
var result = xhr.pipe(function(result) {
if (result.error !== undefined) {
console.error("Server application error", JSON.stringify(result.error));
if (result.error.data.arguments[0] !== "bus.Bus not available in test mode") {
console.error("Server application error", JSON.stringify(result.error));
}
return $.Deferred().reject("server", result.error);
} else {
return result.result;
@@ -8,27 +8,8 @@
// The problem is that we have two web clients in parallel, and every
// addons should behave properly with both.
@odoo-brand-primary: #337ab7;
@odoo-brand-optional: @odoo-brand-primary;
@odoo-brand-secondary: #8f8f8f;
@odoo-brand-lightsecondary: #e2e2e0;
@odoo-list-grey: @odoo-brand-lightsecondary; // FIXME: listview thead background color
@odoo-webclient-background-color: #F0EEEE;
@odoo-view-background-color: white;
@odoo-shadow-color: #303030;
@brand-primary: @odoo-brand-primary;
@odoo-font-size-base: 13px;
@odoo-font-size-base-touch: 15px;
@odoo-main-text-color: #555555;
@odoo-main-color-muted: #a8a8a8;
@odoo-form-group-cols: 12;
@odoo-horizontal-padding: 16px;
@screen-xs-min: 475px;
@screen-xxs-max: (@screen-xs-min - 1);
.oe-view-manager {
background-color: @odoo-webclient-background-color !important;
}
+19
View File
@@ -0,0 +1,19 @@
@odoo-brand-primary: #337ab7;
@odoo-brand-optional: @odoo-brand-primary;
@odoo-brand-secondary: #8f8f8f;
@odoo-brand-lightsecondary: #e2e2e0;
@odoo-list-grey: @odoo-brand-lightsecondary; // FIXME: listview thead background color
@odoo-webclient-background-color: #F0EEEE;
@odoo-view-background-color: white;
@odoo-shadow-color: #303030;
@odoo-font-size-base: 13px;
@odoo-font-size-base-touch: 15px;
@odoo-main-text-color: #555555;
@odoo-main-color-muted: #a8a8a8;
@odoo-form-group-cols: 12;
@odoo-horizontal-padding: 16px;
@screen-xs-min: 475px;
@screen-xxs-max: (@screen-xs-min - 1);
+1
View File
@@ -101,6 +101,7 @@
<link rel="stylesheet" href="/base/static/src/css/modules.css"/>
<link rel="stylesheet" href='/website/static/src/less/import_bootstrap.less'/>
<link rel="stylesheet" href="/web/static/src/less/variables.less"/>
<link rel="stylesheet" href="/web/static/src/less/enterprise_compatibility.less"/>
<link rel="stylesheet" href="/web/static/src/less/utils.less"/>
<link rel="stylesheet" href="/web/static/src/less/modal.less"/>
@@ -22,6 +22,9 @@
<script type="text/javascript" src="/website/static/src/js/website.snippets.animation.js"></script>
<t t-call="web.bootstrap"/>
<link rel="stylesheet" href="/web/static/src/less/variables.less"/>
<link rel="stylesheet" href="/web/static/src/less/utils.less"/>
</template>
<template id="assets_backend" name="website assets for backend" inherit_id="web.assets_backend">
@@ -9,11 +9,12 @@
<template id="assets_frontend" name="im_livechat assets frontend" inherit_id="website.assets_frontend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/bus/static/src/js/bus.js"></script>
<script type="text/javascript" src="/mail/static/src/js/mail_utils.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/chat_window.js"></script>
<script type="text/javascript" src="/mail/static/src/js/thread.js"></script>
<script type="text/javascript" src="/im_livechat/static/src/js/im_livechat.js"></script>
<link rel="stylesheet" href="/mail/static/src/less/mail_chat_common.less"></link>
<link rel="stylesheet" href="/im_livechat/static/src/css/im_livechat.css"/>
<link rel="stylesheet" href="/mail/static/src/less/chat_window.less"></link>
<link rel="stylesheet" href="/mail/static/src/less/thread.less"></link>
<link rel="stylesheet" href="/im_livechat/static/src/less/im_livechat.less"/>
</xpath>
</template>
+4
View File
@@ -226,6 +226,10 @@ class Registry(Mapping):
r, c)
return r, c
def in_test_mode(self):
""" Test whether the registry is in 'test' mode. """
return self.test_cr is not None
def enter_test_mode(self):
""" Enter the 'test' mode, where one cursor serves several requests. """
assert self.test_cr is None