[REF] mail: code refactoring

- extended_chat_window feature in its own files: as it is only available in the
  backend, we don't want it in the frontend assets (which contain the 'basic'
  chat_window)
- move followers feature in its own files
- creation of the 'mail.utils' module in utils.js
- creation of systray.less to define the style of the mail item and dropdown
  in the navbar
- use of chat_manager's search_partner function in PartnerInviteDialog
- some code linting
This commit is contained in:
Aaron Bohy
2016-03-02 13:39:16 +01:00
parent 1e0bc5920f
commit dc81e5f664
21 changed files with 1021 additions and 989 deletions
+2
View File
@@ -61,9 +61,11 @@ Main Features
'static/src/xml/client_action.xml',
'static/src/xml/composer.xml',
'static/src/xml/chatter.xml',
'static/src/xml/followers.xml',
'static/src/xml/systray.xml',
'static/src/xml/thread.xml',
'static/src/xml/chat_window.xml',
'static/src/xml/extended_chat_window.xml',
'static/src/xml/announcement.xml',
],
}
+6 -83
View File
@@ -2,6 +2,7 @@ odoo.define('mail.chat_manager', function (require) {
"use strict";
var bus = require('bus.bus').bus;
var utils = require('mail.utils');
var config = require('web.config');
var core = require('web.core');
var data = require('web.data');
@@ -36,36 +37,13 @@ var global_unread_counter = 0;
var pinned_dm_partners = []; // partner_ids we have a pinned DM with
var client_action_open = false;
// Utils: Window focus/unfocus, beep, tab title, parsing html strings
// Global unread counter and notifications
//----------------------------------------------------------------------------------
var beep = (function () {
if (typeof(Audio) === "undefined") {
return function () {};
}
var audio = new Audio();
var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3";
audio.src = session.url("/mail/static/src/audio/ting" + ext);
return function () { audio.play(); };
})();
bus.on("window_focus", null, function() {
global_unread_counter = 0;
web_client.set_title_part("_chat");
});
// to do: move this to mail.utils
function send_native_notification(title, content) {
var notification = new Notification(title, {body: content, icon: "/mail/static/src/img/odoo_o.png"});
notification.onclick = function (e) {
window.focus();
if (this.cancel) {
this.cancel();
} else if (this.close) {
this.close();
}
};
}
function notify_incoming_message (msg, options) {
if (bus.is_odoo_focused() && options.is_displayed) {
// no need to notify
@@ -75,7 +53,7 @@ function notify_incoming_message (msg, options) {
if (msg.author_id[1]) {
title = _.escape(msg.author_id[1]);
}
var content = parse_and_transform(msg.body, strip_html).substr(0, preview_msg_max_size);
var content = utils.parse_and_transform(msg.body, utils.strip_html).substr(0, preview_msg_max_size);
if (!bus.is_odoo_focused()) {
global_unread_counter++;
@@ -83,60 +61,7 @@ function notify_incoming_message (msg, options) {
web_client.set_title_part("_chat", tab_title);
}
if (Notification && Notification.permission === "granted") {
if (bus.is_master) {
send_native_notification(title, content);
}
} else {
web_client.do_notify(title, content);
if (bus.is_master) {
beep();
}
}
}
function parse_and_transform(html_string, transform_function) {
var open_token = "OPEN" + Date.now();
var string = html_string.replace(/</g, open_token);
var children = $('<div>').html(string).contents();
return _parse_and_transform(children, transform_function)
.replace(new RegExp(open_token, "g"), "&lt;");
}
function _parse_and_transform(nodes, transform_function) {
return _.map(nodes, function (node) {
return transform_function(node, function () {
return _parse_and_transform(node.childNodes, transform_function);
});
}).join("");
}
// suggested regexp (gruber url matching regexp, adapted to js, see https://gist.github.com/gruber/8891611)
var url_regexp = /\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/gi;
function add_link (node, transform_children) {
if (node.nodeType === 3) { // text node
return node.data.replace(url_regexp, function (url) {
var href = (!/^(f|ht)tps?:\/\//i.test(url)) ? "http://" + url : url;
return '<a target="_blank" href="' + href + '">' + url + '</a>';
});
}
if (node.tagName === "A") return node.outerHTML;
node.innerHTML = transform_children();
return node.outerHTML;
}
function strip_html (node, transform_children) {
if (node.nodeType === 3) return node.data; // text node
if (node.tagName === "BR") return "\n";
return transform_children();
}
function inline (node, transform_children) {
if (node.nodeType === 3) return node.data;
if (node.tagName === "BR") return " ";
if (node.tagName.match(/^(A|P|DIV|PRE|BLOCKQUOTE)$/)) return transform_children();
node.innerHTML = transform_children();
return node.outerHTML;
utils.send_notification(title, content);
}
// Message and channel manipulation helpers
@@ -274,7 +199,7 @@ function make_message (data) {
}
// add anchor tags to urls
msg.body = parse_and_transform(msg.body, add_link);
msg.body = utils.parse_and_transform(msg.body, utils.add_link);
// Compute url of attachments
_.each(msg.attachment_ids, function(a) {
@@ -1000,7 +925,7 @@ var chat_manager = {
});
},
get_message_body_preview: function (message_body) {
return parse_and_transform(message_body, inline);
return utils.parse_and_transform(message_body, utils.inline);
},
search_partner: function (search_val, limit) {
@@ -1016,8 +941,6 @@ var chat_manager = {
return values;
});
},
send_native_notification: send_native_notification,
};
chat_manager.bus.on('client_action_open', null, function (open) {
+4 -430
View File
@@ -4,411 +4,13 @@ odoo.define('mail.Chatter', function (require) {
var chat_manager = require('mail.chat_manager');
var composer = require('mail.composer');
var ChatThread = require('mail.ChatThread');
var utils = require('mail.utils');
var ajax = require('web.ajax');
var config = require('web.config');
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 _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.value = [];
this.followers = [];
this.followers_fetched = $.Deferred();
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', '.o_mail_redirect', self.on_click_redirect);
},
on_edit_subtype: function (event) {
var self = this;
var $currentTarget = $(event.currentTarget);
var follower_id = $currentTarget.data('follower-id'); // id of model mail_follower
var res_id = $currentTarget.data('oe-id'); // id of model res_partner or mail_channel
var is_channel = $currentTarget.data('oe-model') === 'mail.channel';
self.dialog = new Dialog(this, {
size: 'medium',
title: _t('Edit Subscription of ') + $currentTarget.siblings('a').text(),
buttons: [
{
text: _t("Apply"),
classes: 'btn-primary',
click: function () {
self.do_update_subscription(event, res_id, is_channel);
},
close: true
},
{
text: _t("Cancel"),
close: true,
},
],
}).open();
return self.fetch_subtypes(follower_id).then(function (data) {
self.display_subtypes(data, true, is_channel);
});
},
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_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);
},
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 () {
var self = this;
this.reinit();
this.value = this.value || [];
return this.fetch_followers().then(function (results) {
self.display_followers(results.followers);
if (results.subtypes) { // current user is follower
self.display_subtypes(results.subtypes);
}
self.display_buttons();
}).fail(this.display_generic.bind(this));
},
fetch_followers: function () {
return ajax.jsonRpc('/mail/read_followers', 'call', {
follower_ids: this.value,
res_model: this.view.model,
res_id: this.view.datarecord.id,
});
},
/** Fetch subtypes of the given follower
* @param {int} [follower_id] the id of the follower
* @param {string} [follower_model] 'res.partner' or 'mail.channel'
*/
fetch_subtypes: function (follower_id) {
return ajax.jsonRpc('/mail/read_subscription_data', 'call', {
res_model: this.view.model,
follower_id: follower_id,
});
},
_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;
},
/** Read on res.partner failed: only display the number of followers */
display_generic: function () {
this.$('.o_followers_actions').hide();
this.$('.o_followers_list').hide();
this.$('.o_followers_title_box > button').prop('disabled', true);
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;
this.trigger('followers_update', 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');
this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', false);
} else {
this.$('button.o_followers_follow_button').removeClass('o_followers_following').addClass('o_followers_notfollow');
this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', true);
}
},
/** Display subtypes: {'name': default, followed} */
display_subtypes:function (data, dialog, display_warning) {
var old_parent_model;
var $list;
if (dialog) {
$list = $('<ul>').appendTo(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) {
this.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,
'display_warning': display_warning && record.internal}))
.appendTo($list);
});
if (display_warning) {
$(qweb.render('mail.Followers.subtypes.warning')).appendTo(this.dialog.$el);
}
},
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).prop("checked", true);
} 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
@@ -506,7 +108,7 @@ var ChatterComposer = composer.BasicComposer.extend({
.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]);
var parsed_email = utils.parse_email(recipient[1]);
if (_.indexOf(email_addresses, parsed_email[1]) === -1) {
self.suggested_partners.push({
checked: true,
@@ -575,7 +177,7 @@ var ChatterComposer = composer.BasicComposer.extend({
var partner_name = partner_info.full_name;
var partner_id = partner_info.partner_id;
var parsed_email = parse_email(partner_name);
var parsed_email = utils.parse_email(partner_name);
var dialog = new form_common.FormViewDialog(self, {
res_model: 'res.partner',
@@ -638,7 +240,7 @@ var ChatterComposer = composer.BasicComposer.extend({
recipient_done.then(function (partner_ids) {
var context = {
default_parent_id: self.id,
default_body: get_text2html(self.$input.val()),
default_body: utils.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,
@@ -928,34 +530,6 @@ var Chatter = form_common.AbstractField.extend({
});
// -----------------------------------------------------------------------------
// 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;
+3 -4
View File
@@ -4,6 +4,7 @@ odoo.define('mail.chat_client_action', function (require) {
var chat_manager = require('mail.chat_manager');
var composer = require('mail.composer');
var ChatThread = require('mail.ChatThread');
var utils = require('mail.utils');
var config = require('web.config');
var ControlPanelMixin = require('web.ControlPanelMixin');
@@ -42,10 +43,8 @@ var PartnerInviteDialog = Dialog.extend({
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%',
@@ -56,7 +55,7 @@ var PartnerInviteDialog = Dialog.extend({
return $('<span>').text(item.text).prepend(status);
},
query: function (query) {
self.PartnersModel.call('im_search', [query.term, 20]).then(function(result){
chat_manager.search_partner(query.term, 20).then(function(result){
var data = [];
_.each(result, function(partner){
partner.text = partner.name;
@@ -125,7 +124,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
var def = window.Notification.requestPermission();
if (def) {
def.then(function () {
chat_manager.send_native_notification('Permission granted', 'Odoo has now the permission to send you native notifications on this device.');
utils.send_native_notification('Permission granted', 'Odoo has now the permission to send you native notifications on this device.');
});
}
},
+3 -22
View File
@@ -2,6 +2,7 @@ odoo.define('mail.composer', function (require) {
"use strict";
var chat_manager = require('mail.chat_manager');
var utils = require('mail.utils');
var core = require('web.core');
var data = require('web.data');
@@ -13,20 +14,6 @@ var Widget = require('web.Widget');
var QWeb = core.qweb;
var _t = core._t;
var accented_letters_mapping = {
'a': '[àáâãäå]',
'ae': 'æ',
'c': 'ç',
'e': '[èéêë]',
'i': '[ìíîï]',
'n': 'ñ',
'o': '[òóôõö]',
'oe': 'œ',
'u': '[ùúûűü]',
'y': '[ýÿ]',
' ': '[()\\[\\]]',
};
var NON_BREAKING_SPACE = '\u00a0';
var MENTION_PARTNER_DELIMITER = '@';
@@ -614,12 +601,12 @@ var BasicComposer = Widget.extend({
// filter prefetched partners with the given search string
var suggestions = [];
var limit = self.options.mention_fetch_limit;
var search_regexp = new RegExp(self.unaccent(search), 'i');
var search_regexp = new RegExp(utils.unaccent(search), 'i');
_.each(prefetched_partners, function (partners) {
if (limit > 0) {
var filtered_partners = _.filter(partners, function (partner) {
return partner.email && partner.email.search(search_regexp) !== -1 ||
partner.name && self.unaccent(partner.name).search(search_regexp) !== -1;
partner.name && utils.unaccent(partner.name).search(search_regexp) !== -1;
});
if (filtered_partners.length) {
suggestions.push(filtered_partners.slice(0, limit));
@@ -652,12 +639,6 @@ var BasicComposer = Widget.extend({
focus: function () {
this.$input.focus();
},
unaccent: function (str) {
_.each(accented_letters_mapping, function (value, key) {
str = str.replace(new RegExp(value, 'g'), key);
});
return str;
},
});
var ExtendedComposer = BasicComposer.extend({
@@ -0,0 +1,30 @@
odoo.define('mail.ExtendedChatWindow', function (require) {
"use strict";
var chat_manager = require('mail.chat_manager');
var ChatWindow = require('mail.ChatWindow');
return ChatWindow.extend({
template: "mail.ExtendedChatWindow",
start: function () {
var self = this;
return this._super().then(function () {
if (self.options.thread_less) {
self.$el.addClass('o_thread_less');
self.$('.o_chat_search_input input')
.autocomplete({
source: function(request, response) {
chat_manager.search_partner(request.term, 10).done(response);
},
select: function(event, ui) {
self.trigger('open_dm_session', ui.item.id);
},
})
.focus();
}
});
},
});
});
+411
View File
@@ -0,0 +1,411 @@
odoo.define('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 _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.value = [];
this.followers = [];
this.followers_fetched = $.Deferred();
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', '.o_mail_redirect', self.on_click_redirect);
},
on_edit_subtype: function (event) {
var self = this;
var $currentTarget = $(event.currentTarget);
var follower_id = $currentTarget.data('follower-id'); // id of model mail_follower
var res_id = $currentTarget.data('oe-id'); // id of model res_partner or mail_channel
var is_channel = $currentTarget.data('oe-model') === 'mail.channel';
self.dialog = new Dialog(this, {
size: 'medium',
title: _t('Edit Subscription of ') + $currentTarget.siblings('a').text(),
buttons: [
{
text: _t("Apply"),
classes: 'btn-primary',
click: function () {
self.do_update_subscription(event, res_id, is_channel);
},
close: true
},
{
text: _t("Cancel"),
close: true,
},
],
}).open();
return self.fetch_subtypes(follower_id).then(function (data) {
self.display_subtypes(data, true, is_channel);
});
},
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_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);
},
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 () {
var self = this;
this.reinit();
this.value = this.value || [];
return this.fetch_followers().then(function (results) {
self.display_followers(results.followers);
if (results.subtypes) { // current user is follower
self.display_subtypes(results.subtypes);
}
self.display_buttons();
}).fail(this.display_generic.bind(this));
},
fetch_followers: function () {
return ajax.jsonRpc('/mail/read_followers', 'call', {
follower_ids: this.value,
res_model: this.view.model,
res_id: this.view.datarecord.id,
});
},
/** Fetch subtypes of the given follower
* @param {int} [follower_id] the id of the follower
* @param {string} [follower_model] 'res.partner' or 'mail.channel'
*/
fetch_subtypes: function (follower_id) {
return ajax.jsonRpc('/mail/read_subscription_data', 'call', {
res_model: this.view.model,
follower_id: follower_id,
});
},
_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;
},
/** Read on res.partner failed: only display the number of followers */
display_generic: function () {
this.$('.o_followers_actions').hide();
this.$('.o_followers_list').hide();
this.$('.o_followers_title_box > button').prop('disabled', true);
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;
this.trigger('followers_update', 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');
this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', false);
} else {
this.$('button.o_followers_follow_button').removeClass('o_followers_following').addClass('o_followers_notfollow');
this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', true);
}
},
/** Display subtypes: {'name': default, followed} */
display_subtypes:function (data, dialog, display_warning) {
var old_parent_model;
var $list;
if (dialog) {
$list = $('<ul>').appendTo(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) {
this.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,
'display_warning': display_warning && record.internal}))
.appendTo($list);
});
if (display_warning) {
$(qweb.render('mail.Followers.subtypes.warning')).appendTo(this.dialog.$el);
}
},
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).prop("checked", true);
} 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'));
}
},
});
core.form_widget_registry.add('mail_followers', Followers);
});
+87 -87
View File
@@ -53,100 +53,100 @@ SystrayMenu.Items.push(InboxItem);
* contains a direct link to the Inbox in Discuss.
**/
var MessagingMenu = Widget.extend({
template:'mail.chat.MessagingMenu',
events: {
"click": "on_click",
"click .o_filter_button": "on_click_filter_button",
"click .o_new_message": "on_click_new_message",
"click .o_mail_channel_preview": "on_click_channel",
},
start: function () {
this.$filter_buttons = this.$('.o_filter_button');
this.$channels_preview = this.$('.o_mail_navbar_dropdown_channels');
this.filter = false;
chat_manager.bus.on("update_channel_unread_counter", this, this.update_counter);
chat_manager.is_ready.then(this.update_counter.bind(this));
return this._super();
},
is_open: function () {
return this.$el.hasClass('open');
},
update_counter: function () {
var counter = chat_manager.get_unread_conversation_counter();
this.$('.o_notification_counter').text(counter);
this.$el.toggleClass('o_no_notification', !counter);
this.$el.toggleClass('o_unread_chat', !!chat_manager.get_chat_unread_counter());
if (this.is_open()) {
this.update_channels_preview();
}
},
update_channels_preview: function () {
var self = this;
template:'mail.chat.MessagingMenu',
events: {
"click": "on_click",
"click .o_filter_button": "on_click_filter_button",
"click .o_new_message": "on_click_new_message",
"click .o_mail_channel_preview": "on_click_channel",
},
start: function () {
this.$filter_buttons = this.$('.o_filter_button');
this.$channels_preview = this.$('.o_mail_navbar_dropdown_channels');
this.filter = false;
chat_manager.bus.on("update_channel_unread_counter", this, this.update_counter);
chat_manager.is_ready.then(this.update_counter.bind(this));
return this._super();
},
is_open: function () {
return this.$el.hasClass('open');
},
update_counter: function () {
var counter = chat_manager.get_unread_conversation_counter();
this.$('.o_notification_counter').text(counter);
this.$el.toggleClass('o_no_notification', !counter);
this.$el.toggleClass('o_unread_chat', !!chat_manager.get_chat_unread_counter());
if (this.is_open()) {
this.update_channels_preview();
}
},
update_channels_preview: function () {
var self = this;
// Display spinner while waiting for channels preview
this.$channels_preview.html(QWeb.render('mail.chat.Spinner'));
// Display spinner while waiting for channels preview
this.$channels_preview.html(QWeb.render('mail.chat.Spinner'));
chat_manager.is_ready.then(function () {
var channels = _.filter(chat_manager.get_channels(), function (channel) {
if (self.filter === 'chat') {
return channel.is_chat;
} else if (self.filter === 'channels') {
return !channel.is_chat && channel.type !== 'static';
} else {
return channel.type !== 'static';
}
});
chat_manager.get_channels_preview(channels).then(self._render_channels_preview.bind(self));
});
},
_render_channels_preview: function (channels_preview) {
// Sort channels: 1. channels with unread messages, 2. chat, 3. by date of last msg
channels_preview.sort(function (c1, c2) {
return Math.min(1, c2.unread_counter) - Math.min(1, c1.unread_counter) ||
c2.is_chat - c1.is_chat ||
c2.last_message.date.diff(c1.last_message.date);
});
// Generate last message preview (inline message body and compute date to display)
_.each(channels_preview, function (channel) {
channel.last_message_preview =
channel.last_message.displayed_author + ': ' +
chat_manager.get_message_body_preview(channel.last_message.body);
if (channel.last_message.date.isSame(new Date(), 'd')) { // today
channel.last_message_date = channel.last_message.date.format('LT');
chat_manager.is_ready.then(function () {
var channels = _.filter(chat_manager.get_channels(), function (channel) {
if (self.filter === 'chat') {
return channel.is_chat;
} else if (self.filter === 'channels') {
return !channel.is_chat && channel.type !== 'static';
} else {
channel.last_message_date = channel.last_message.date.format('lll');
return channel.type !== 'static';
}
});
this.$channels_preview.html(QWeb.render('mail.chat.ChannelsPreview', {
channels: channels_preview,
}));
},
on_click: function () {
if (!this.is_open()) {
this.update_channels_preview(); // we are opening the dropdown so update its content
chat_manager.get_channels_preview(channels).then(self._render_channels_preview.bind(self));
});
},
_render_channels_preview: function (channels_preview) {
// Sort channels: 1. channels with unread messages, 2. chat, 3. by date of last msg
channels_preview.sort(function (c1, c2) {
return Math.min(1, c2.unread_counter) - Math.min(1, c1.unread_counter) ||
c2.is_chat - c1.is_chat ||
c2.last_message.date.diff(c1.last_message.date);
});
// Generate last message preview (inline message body and compute date to display)
_.each(channels_preview, function (channel) {
channel.last_message_preview =
channel.last_message.displayed_author + ': ' +
chat_manager.get_message_body_preview(channel.last_message.body);
if (channel.last_message.date.isSame(new Date(), 'd')) { // today
channel.last_message_date = channel.last_message.date.format('LT');
} else {
channel.last_message_date = channel.last_message.date.format('lll');
}
},
on_click_filter_button: function (event) {
event.stopPropagation();
this.$filter_buttons.removeClass('o_selected');
var $target = $(event.currentTarget);
$target.addClass('o_selected');
this.filter = $target.data('filter');
this.update_channels_preview();
},
on_click_new_message: function () {
chat_manager.bus.trigger('open_chat');
},
on_click_channel: function (event) {
var channel_id = $(event.currentTarget).data('channel_id');
var channel = chat_manager.get_channel(channel_id);
if (channel) {
chat_manager.open_channel(channel);
}
},
});
this.$channels_preview.html(QWeb.render('mail.chat.ChannelsPreview', {
channels: channels_preview,
}));
},
on_click: function () {
if (!this.is_open()) {
this.update_channels_preview(); // we are opening the dropdown so update its content
}
},
on_click_filter_button: function (event) {
event.stopPropagation();
this.$filter_buttons.removeClass('o_selected');
var $target = $(event.currentTarget);
$target.addClass('o_selected');
this.filter = $target.data('filter');
this.update_channels_preview();
},
on_click_new_message: function () {
chat_manager.bus.trigger('open_chat');
},
on_click_channel: function (event) {
var channel_id = $(event.currentTarget).data('channel_id');
var channel = chat_manager.get_channel(channel_id);
if (channel) {
chat_manager.open_channel(channel);
}
},
});
SystrayMenu.Items.push(MessagingMenu);
+137
View File
@@ -0,0 +1,137 @@
odoo.define('mail.utils', function (require) {
"use strict";
var bus = require('bus.bus');
var session = require('web.session');
var web_client = require('web.web_client');
function send_notification(title, content) {
if (Notification && Notification.permission === "granted") {
if (bus.is_master) {
_send_native_notification(title, content);
}
} else {
web_client.do_notify(title, content);
if (bus.is_master) {
_beep();
}
}
}
function _send_native_notification(title, content) {
var notification = new Notification(title, {body: content, icon: "/mail/static/src/img/odoo_o.png"});
notification.onclick = function () {
window.focus();
if (this.cancel) {
this.cancel();
} else if (this.close) {
this.close();
}
};
}
var _beep = (function () {
if (typeof(Audio) === "undefined") {
return function () {};
}
var audio = new Audio();
var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3";
audio.src = session.url("/mail/static/src/audio/ting" + ext);
return function () { audio.play(); };
})();
function parse_and_transform(html_string, transform_function) {
var open_token = "OPEN" + Date.now();
var string = html_string.replace(/&lt;/g, open_token);
var children = $('<div>').html(string).contents();
return _parse_and_transform(children, transform_function)
.replace(new RegExp(open_token, "g"), "&lt;");
}
function _parse_and_transform(nodes, transform_function) {
return _.map(nodes, function (node) {
return transform_function(node, function () {
return _parse_and_transform(node.childNodes, transform_function);
});
}).join("");
}
// suggested regexp (gruber url matching regexp, adapted to js, see https://gist.github.com/gruber/8891611)
var url_regexp = /\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/gi;
function add_link (node, transform_children) {
if (node.nodeType === 3) { // text node
return node.data.replace(url_regexp, function (url) {
var href = (!/^(f|ht)tps?:\/\//i.test(url)) ? "http://" + url : url;
return '<a target="_blank" href="' + href + '">' + url + '</a>';
});
}
if (node.tagName === "A") return node.outerHTML;
node.innerHTML = transform_children();
return node.outerHTML;
}
function strip_html (node, transform_children) {
if (node.nodeType === 3) return node.data; // text node
if (node.tagName === "BR") return "\n";
return transform_children();
}
function inline (node, transform_children) {
if (node.nodeType === 3) return node.data;
if (node.tagName === "BR") return " ";
if (node.tagName.match(/^(A|P|DIV|PRE|BLOCKQUOTE)$/)) return transform_children();
node.innerHTML = transform_children();
return node.outerHTML;
}
// Parses 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/>');
}
var accented_letters_mapping = {
'a': '[àáâãäå]',
'ae': 'æ',
'c': 'ç',
'e': '[èéêë]',
'i': '[ìíîï]',
'n': 'ñ',
'o': '[òóôõö]',
'oe': 'œ',
'u': '[ùúûűü]',
'y': '[ýÿ]',
' ': '[()\\[\\]]',
};
function unaccent (str) {
_.each(accented_letters_mapping, function (value, key) {
str = str.replace(new RegExp(value, 'g'), key);
});
return str;
}
return {
send_notification: send_notification,
parse_and_transform: parse_and_transform,
add_link: add_link,
strip_html: strip_html,
inline: inline,
parse_email: parse_email,
get_text2html: get_text2html,
unaccent: unaccent,
};
});
@@ -398,34 +398,3 @@ core.bus.on('web_client_ready', null, function () {
});
});
// FIXME: move this to its own file in master
odoo.define('mail.ExtendedChatWindow', function (require) {
"use strict";
var chat_manager = require('mail.chat_manager');
var ChatWindow = require('mail.ChatWindow');
return ChatWindow.extend({
template: "mail.ExtendedChatWindow",
start: function () {
var self = this;
return this._super().then(function () {
if (self.options.thread_less) {
self.$el.addClass('o_thread_less');
self.$('.o_chat_search_input input')
.autocomplete({
source: function(request, response) {
chat_manager.search_partner(request.term, 10).done(response);
},
select: function(event, ui) {
self.trigger('open_dm_session', ui.item.id);
},
})
.focus();
}
});
},
});
});
@@ -135,24 +135,3 @@
.o_no_chat_window .o_chat_window {
display: none;
}
// FIXME: move this to its own file in master
.o_chat_window.o_thread_less {
.o_chat_search_input {
.o-flex-display();
padding: 5px;
> span {
.o-flex(0, 0, auto);
}
> input {
margin-left: 5px;
.o-flex(1, 1, 100%);
}
}
.o_mail_thread {
background-color: lightgray;
}
.o_chat_input {
display: none;
}
}
-91
View File
@@ -40,94 +40,3 @@
margin-top: 10px;
}
}
.o_followers {
float: right;
@media (max-width: @screen-xs-max) {
display: none !important; // Hide the followers in mobile
}
.o_followers_actions {
position: relative;
float: right;
.o_followers_follow_button {
width: 120px;
.o_follow, .o_followers_unfollow, .o_followers_following {
display: none;
}
// when following, show 'following'
&.o_followers_following .o_followers_following {
display: inline;
}
// when following and hovering, show 'unfollow'
&.o_followers_following:hover .o_followers_following {
display: none;
}
&.o_followers_following:hover .o_followers_unfollow {
display: inline;
}
// when not following show 'follow'
&.o_followers_notfollow .o_follow {
display: inline;
}
}
.o_subtype {
padding-left: 10px;
}
}
.o_followers_title_box {
position: relative;
float: right;
margin-left: 5px;
> button {
width: 140px;
}
.o_followers_count {
padding-right: 5px;
}
.o_followers_list {
font-size: small;
.o_partner {
@partner_avatar_size: 24px;
position: relative; // required for old design compatibility
> a {
max-width: 160px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
height: @partner_avatar_size;
padding: 3px @partner_avatar_size + 8px;
}
> img {
.o-position-absolute();
width: @partner_avatar_size;
}
.o_remove_follower {
.o-position-absolute(6px, 5px);
cursor: pointer;
}
.o_edit_subtype {
.o-position-absolute(6px, 18px);
cursor: pointer;
}
}
}
}
}
// Edit subtypes in dialog
.modal .modal-body > ul {
padding-left: 0;
> li.o_subtype {
list-style: none;
}
}
+1 -121
View File
@@ -274,7 +274,7 @@
}
}
// User status bullet (in Discuss sidebar and Invite dialog)
// User status bullet
.o_mail_user_status {
font-size: 10px;
position: relative;
@@ -286,123 +286,3 @@
color: orange;
}
}
// Navbar icon and dropdown (FIXME: move to systray.less)
.o_mail_navbar_item {
> a {
opacity: 1;
> i {
font-size: larger;
}
}
&.o_no_notification > a {
opacity: 0.5;
.o_notification_counter {
display: none;
}
}
&.o_unread_chat > a > i {
color: @odoo-brand-optional;
}
&.open .o_mail_navbar_dropdown {
.o-flex-display();
.o-flex-flow(column, nowrap);
}
.o_mail_navbar_dropdown {
width: 350px;
padding: 0;
.o_spinner {
.o-flex-display();
.o-align-items(center);
.o-justify-content(center);
height: 50px;
}
.o_mail_navbar_dropdown_top {
.o-flex-display();
.o-flex(0, 0, auto);
justify-content: space-between;
border-bottom: 1px solid lightgray;
.o_filter_button, .o_new_message {
.btn-link;
padding: 5px;
}
.o_filter_button {
color: @odoo-main-color-muted;
&:hover, &.o_selected {
color: @odoo-brand-optional;
}
&.o_selected {
cursor: default;
font-weight: bold;
}
}
}
.o_mail_navbar_dropdown_channels {
.o-flex(0, 1, auto);
max-height: 400px;
min-height: 50px;
overflow-y: auto;
@media (max-width: @screen-xs-max) {
max-height: none;
}
.o_mail_channel_preview {
.o-flex-display();
height: 50px;
align-items: center;
padding: 5px;
cursor: pointer;
font-size: 12px;
overflow: hidden;
&~.o_mail_channel_preview {
border-top: 1px solid lightgray;
}
&.o_channel_unread {
background-color: lighten(lightgray, 10%);
}
&:hover {
background-color: lighten(lightgray, 5%);
}
.o_channel_image {
.o-flex(0, 0, 36px);
max-height: 36px;
}
.o_channel_info {
.o-flex(1, 1, 100%);
margin-left: 5px;
overflow: hidden;
.o_channel_title {
.o-flex-display();
.o_channel_name {
.o-flex(1, 1, auto);
.o-text-overflow();
}
.o_last_message_date {
.o-flex(0, 0, auto);
padding-top: 3px;
font-size: xx-small;
color: @odoo-main-color-muted;
}
}
.o_last_message_preview {
width: 100%;
color: @odoo-main-color-muted;
.o-text-overflow();
}
}
}
}
}
}
.o_no_chat_window .o_mail_navbar_dropdown .o_new_message {
display: none; // hide 'new message' button if chat windows are disabled
}
@@ -0,0 +1,19 @@
.o_chat_window.o_thread_less {
.o_chat_search_input {
.o-flex-display();
padding: 5px;
> span {
.o-flex(0, 0, auto);
}
> input {
margin-left: 5px;
.o-flex(1, 1, 100%);
}
}
.o_mail_thread {
background-color: lightgray;
}
.o_chat_input {
display: none;
}
}
@@ -0,0 +1,90 @@
.o_followers {
float: right;
@media (max-width: @screen-xs-max) {
display: none !important; // Hide the followers in mobile
}
.o_followers_actions {
position: relative;
float: right;
.o_followers_follow_button {
width: 120px;
.o_follow, .o_followers_unfollow, .o_followers_following {
display: none;
}
// when following, show 'following'
&.o_followers_following .o_followers_following {
display: inline;
}
// when following and hovering, show 'unfollow'
&.o_followers_following:hover .o_followers_following {
display: none;
}
&.o_followers_following:hover .o_followers_unfollow {
display: inline;
}
// when not following show 'follow'
&.o_followers_notfollow .o_follow {
display: inline;
}
}
.o_subtype {
padding-left: 10px;
}
}
.o_followers_title_box {
position: relative;
float: right;
margin-left: 5px;
> button {
width: 140px;
}
.o_followers_count {
padding-right: 5px;
}
.o_followers_list {
font-size: small;
.o_partner {
@partner_avatar_size: 24px;
position: relative; // required for old design compatibility
> a {
max-width: 160px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
height: @partner_avatar_size;
padding: 3px @partner_avatar_size + 8px;
}
> img {
.o-position-absolute();
width: @partner_avatar_size;
}
.o_remove_follower {
.o-position-absolute(6px, 5px);
cursor: pointer;
}
.o_edit_subtype {
.o-position-absolute(6px, 18px);
cursor: pointer;
}
}
}
}
}
// Edit subtypes in dialog
.modal .modal-body > ul {
padding-left: 0;
> li.o_subtype {
list-style: none;
}
}
+119
View File
@@ -0,0 +1,119 @@
// Navbar icon and dropdown
.o_mail_navbar_item {
> a {
opacity: 1;
> i {
font-size: larger;
}
}
&.o_no_notification > a {
opacity: 0.5;
.o_notification_counter {
display: none;
}
}
&.o_unread_chat > a > i {
color: @odoo-brand-optional;
}
&.open .o_mail_navbar_dropdown {
.o-flex-display();
.o-flex-flow(column, nowrap);
}
.o_mail_navbar_dropdown {
width: 350px;
padding: 0;
.o_spinner {
.o-flex-display();
.o-align-items(center);
.o-justify-content(center);
height: 50px;
}
.o_mail_navbar_dropdown_top {
.o-flex-display();
.o-flex(0, 0, auto);
justify-content: space-between;
border-bottom: 1px solid lightgray;
.o_filter_button, .o_new_message {
.btn-link;
padding: 5px;
}
.o_filter_button {
color: @odoo-main-color-muted;
&:hover, &.o_selected {
color: @odoo-brand-optional;
}
&.o_selected {
cursor: default;
font-weight: bold;
}
}
}
.o_mail_navbar_dropdown_channels {
.o-flex(0, 1, auto);
max-height: 400px;
min-height: 50px;
overflow-y: auto;
@media (max-width: @screen-xs-max) {
max-height: none;
}
.o_mail_channel_preview {
.o-flex-display();
height: 50px;
align-items: center;
padding: 5px;
cursor: pointer;
font-size: 12px;
overflow: hidden;
&~.o_mail_channel_preview {
border-top: 1px solid lightgray;
}
&.o_channel_unread {
background-color: lighten(lightgray, 10%);
}
&:hover {
background-color: lighten(lightgray, 5%);
}
.o_channel_image {
.o-flex(0, 0, 36px);
max-height: 36px;
}
.o_channel_info {
.o-flex(1, 1, 100%);
margin-left: 5px;
overflow: hidden;
.o_channel_title {
.o-flex-display();
.o_channel_name {
.o-flex(1, 1, auto);
.o-text-overflow();
}
.o_last_message_date {
.o-flex(0, 0, auto);
padding-top: 3px;
font-size: xx-small;
color: @odoo-main-color-muted;
}
}
.o_last_message_preview {
width: 100%;
color: @odoo-main-color-muted;
.o-text-overflow();
}
}
}
}
}
}
.o_no_chat_window .o_mail_navbar_dropdown .o_new_message {
display: none; // hide 'new message' button if chat windows are disabled
}
@@ -18,16 +18,6 @@
</div>
</t>
<!-- FIXME: move this to its own file in master -->
<t t-name="mail.ExtendedChatWindow" t-extend="mail.ChatWindow">
<t t-jquery=".o_chat_header" t-operation="after">
<div t-if="widget.options.thread_less" class="o_chat_search_input">
<span> To: </span>
<input type="text"/>
</div>
</t>
</t>
<t t-name="mail.ChatWindowsDropdown">
<div t-attf-class="o_chat_window o_chat_window_dropdown dropup #{open ? 'open' : ''}">
<span class="o_chat_window_dropdown_toggler dropdown-toggle" data-toggle="dropdown">
-89
View File
@@ -74,93 +74,4 @@
</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 class="o_followers_title_box">
<button 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 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 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"
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-follower-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-esc="record.name"/>
</span>
<i t-if="display_warning" class="fa fa-warning text-warning"/>
</li>
</t>
<t t-name="mail.Followers.subtypes.warning">
<span class="text-warning">
<i class="fa fa-warning"/> Be careful with channels following internal notifications
</span>
</t>
</templates>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ExtendedChatWindow" t-extend="mail.ChatWindow">
<t t-jquery=".o_chat_header" t-operation="after">
<div t-if="widget.options.thread_less" class="o_chat_search_input">
<span> To: </span>
<input type="text"/>
</div>
</t>
</t>
</templates>
+90
View File
@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<!--
Followers, actions and subtypes in a record.
-->
<t t-name="mail.Followers">
<div class="o_followers">
<div class="o_followers_title_box">
<button 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 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 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>
<!--
Partner or channel 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"
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-follower-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>
<!--
'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>
<!--
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-esc="record.name"/>
</span>
<i t-if="display_warning" class="fa fa-warning text-warning"/>
</li>
</t>
<t t-name="mail.Followers.subtypes.warning">
<span class="text-warning">
<i class="fa fa-warning"/> Be careful with channels following internal notifications
</span>
</t>
</templates>
+6
View File
@@ -8,19 +8,25 @@
<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/extended_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/followers.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/utils.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/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/extended_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/followers.less" type="text/less"/>
<link rel="stylesheet" href="/mail/static/src/less/thread.less" type="text/less"/>
<link rel="stylesheet" href="/mail/static/src/less/systray.less" type="text/less"/>
</xpath>
</template>
</data>