[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:
@@ -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',
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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"), "<");
|
||||
}
|
||||
|
||||
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,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;
|
||||
|
||||
@@ -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.');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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(/</g, open_token);
|
||||
var children = $('<div>').html(string).contents();
|
||||
return _parse_and_transform(children, transform_function)
|
||||
.replace(new RegExp(open_token, "g"), "<");
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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 > 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>
|
||||
@@ -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 > 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user