[REF][IMP] *: mail js refactoring

This commit improves the JS code of the mail module,
which comes maily from the new coding guidelines
and the addition of "JS services".

JS services are important objects that do not fit
well in the component tree, such as chat_manager
or ajax.

The benefits of JS services are improved readability
of the code, reduced coupling, and more testable
modules. In particular, discuss was hard to

Summary of the changes:

- ClientAction has been renamed into Discuss
- Clear instantiation of chatManager
- New coding guidelines in most mail modules
- JS Services can interact with each other
- Chat Manager and Window Manager are services
- Window Manager renamed to Chat Window Manager
- bus.bus is now encapsulated in Bus Service (a service)
- The test infrastructure has been tweaked with JS Services
- Chat Mixin has been removed

A future improvement would be to translate some 'trigger' into 'trigger_up'.
This commit is contained in:
Aaron Bohy
2018-02-01 14:51:58 +01:00
committed by Alexandre Kühn
parent 0cd656b5af
commit 02ec09cb1c
52 changed files with 3353 additions and 2583 deletions
@@ -0,0 +1,96 @@
odoo.define('bus.BusService', function (require) {
"use strict";
var bus = require('bus.bus').bus;
var AbstractService = require('web.AbstractService');
var BusService = AbstractService.extend({
name: 'bus_service',
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
this.bus = bus;
this._audio = null;
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Get the bus
*
* @return {web.Bus}
*/
getBus: function () {
return this.bus;
},
/**
* Send a notification, and notify once per browser's tab
*
* @param {web.Widget} widget the widget that called sendNotification
* @param {string} title
* @param {string} content
*/
sendNotification: function (widget, title, content) {
if (window.Notification && Notification.permission === "granted") {
if (this.bus.is_master) {
this._sendNativeNotification(title, content);
}
} else {
widget.do_notify(title, content);
if (this.bus.is_master) {
this._beep(widget);
}
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Lazily play the 'beep' audio on sent notification
*
* @private
* @param {web.Widget} widget
*/
_beep: function (widget) {
if (typeof(Audio) !== "undefined") {
if (!this._audio) {
this._audio = new Audio();
var ext = this._audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3";
var session = widget.getSession();
this._audio.src = session.url("/mail/static/src/audio/ting" + ext);
}
this._audio.play();
}
},
/**
* Show a browser notification
*
* @private
* @param {string} title
* @param {string} content
*/
_sendNativeNotification: function (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();
}
};
},
});
return BusService;
});
+1
View File
@@ -4,6 +4,7 @@
<template id="assets_backend" name="im assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/bus/static/src/js/bus.js"></script>
<script type="text/javascript" src="/bus/static/src/js/services/bus_service.js"></script>
</xpath>
</template>
</data>
@@ -1,7 +1,6 @@
odoo.define('base_calendar.base_calendar', function (require) {
"use strict";
var bus = require('bus.bus').bus;
var BasicModel = require('web.BasicModel');
var field_registry = require('web.field_registry');
var Notification = require('web.Notification');
@@ -99,7 +98,7 @@ WebClient.include({
// in which the current user is involved is created, edited or deleted
this.calendar_notif_timeouts = {};
this.calendar_notif = {};
bus.on('notification', this, function (notifications) {
this.call('bus_service', 'getBus').on('notification', this, function (notifications) {
_.each(notifications, (function (notification) {
if (notification[0][1] === 'calendar.alarm') {
this.display_calendar_notif(notification[1]);
+16 -13
View File
@@ -1,10 +1,9 @@
odoo.define('im_livechat.im_livechat', function (require) {
"use strict";
var local_storage = require('web.local_storage');
var bus = require('bus.bus').bus;
var config = require('web.config');
var core = require('web.core');
var ServiceProviderMixin = require('web.ServiceProviderMixin');
var session = require('web.session');
var time = require('web.time');
var utils = require('web.utils');
@@ -20,10 +19,10 @@ var LIVECHAT_COOKIE_HISTORY = 'im_livechat_history';
var HISTORY_LIMIT = 15;
// History tracking
var page = window.location.href.replace(/^.*\/\/[^\/]+/, '');
var page = window.location.href.replace(/^.*\/\/[^/]+/, '');
var page_history = utils.get_cookie(LIVECHAT_COOKIE_HISTORY);
var url_history = [];
if(page_history){
if (page_history) {
url_history = JSON.parse(page_history) || [];
}
if (!_.contains(url_history, page)) {
@@ -34,8 +33,11 @@ if (!_.contains(url_history, page)) {
utils.set_cookie(LIVECHAT_COOKIE_HISTORY, JSON.stringify(url_history), 60*60*24); // 1 day cookie
}
var LivechatButton = Widget.extend({
/**
* In order to handle services like ajax in the external lib,
* The parent of Livechat, LivechatButton, is set as a service provider
*/
var LivechatButton = Widget.extend(ServiceProviderMixin, {
className:"openerp o_livechat_button hidden-print",
events: {
@@ -54,6 +56,7 @@ var LivechatButton = Widget.extend({
this.chat_window = null;
this.messages = [];
this.server_url = server_url;
this.busBus = this.call('bus_service', 'getBus');
},
willStart: function () {
@@ -87,7 +90,7 @@ var LivechatButton = Widget.extend({
this.auto_popup_timeout = setTimeout(this.open_chat.bind(this), this.rule.auto_popup_timer*1000);
}
}
bus.on('notification', this, function (notifications) {
this.busBus.on('notification', this, function (notifications) {
var self = this;
_.each(notifications, function (notification) {
self._on_notification(notification);
@@ -95,9 +98,9 @@ var LivechatButton = Widget.extend({
});
return this._super();
},
_on_notification: function(notification){
_on_notification: function (notification){
if (this.channel && (notification[0] === this.channel.uuid)) {
if(notification[1]._type === "history_command") { // history request
if (notification[1]._type === "history_command") { // history request
var cookie = utils.get_cookie(LIVECHAT_COOKIE_HISTORY);
var history = cookie ? JSON.parse(cookie) : [];
session.rpc("/im_livechat/history", {
@@ -105,7 +108,7 @@ var LivechatButton = Widget.extend({
channel_uuid: this.channel.uuid,
page_history: history,
});
}else{ // normal message
} else { // normal message
this.add_message(notification[1]);
this.render_messages();
if (this.chat_window.folded || !this.chat_window.thread.is_at_bottom()) {
@@ -114,7 +117,7 @@ var LivechatButton = Widget.extend({
}
}
},
load_qweb_template: function(){
load_qweb_template: function (){
var xml_files = ['/mail/static/src/xml/chat_window.xml',
'/mail/static/src/xml/thread.xml',
'/im_livechat/static/src/xml/im_livechat.xml'];
@@ -153,8 +156,8 @@ var LivechatButton = Widget.extend({
self.send_welcome_message();
self.render_messages();
bus.add_channel(channel.uuid);
bus.start_polling();
this.busBus.add_channel(channel.uuid);
this.busBus.start_polling();
utils.set_cookie('im_livechat_session', JSON.stringify(channel), 60*60);
utils.set_cookie('im_livechat_auto_popup', JSON.stringify(false), 60*60);
@@ -1,11 +1,9 @@
odoo.define('im_livechat.chat_client_action', function (require) {
odoo.define('im_livechat.chat_discuss', function (require) {
"use strict";
require('mail.chat_client_action');
var chat_manager = require('mail.chat_manager');
var core = require('web.core');
var Discuss = require('mail.chat_discuss');
core.action_registry.get('mail.chat.instant_messaging').include({
Discuss.include({
_renderSidebar: function (options) {
// Override to sort livechat channels by last message's date
var channel_partition = _.partition(options.channels, function (channel) {
@@ -19,13 +17,4 @@ core.action_registry.get('mail.chat.instant_messaging').include({
},
});
chat_manager.bus.on('new_message', null, function (msg) {
_.each(msg.channel_ids, function (channel_id) {
var channel = chat_manager.get_channel(channel_id);
if (channel) {
channel.last_message_date = msg.date; // update the last message's date of the channel
}
});
});
});
@@ -14,7 +14,7 @@
</t>
<!-- Mobile templates -->
<t t-extend="mail.client_action_mobile">
<t t-extend="mail.discuss_mobile">
<t t-jquery=".o_mail_mobile_tabs" t-operation="append">
<div class="o_mail_mobile_tab" data-type="livechat">
<span class="fa fa-comments"/>
@@ -128,6 +128,7 @@
<script type="text/javascript" src="/web/static/src/js/core/ajax.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/time.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/mixins.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/service_mixins.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/rpc.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/widget.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/registry.js"></script>
@@ -141,6 +142,7 @@
<script type="text/javascript" src="/web/static/src/js/services/local_storage_service.js"></script>
<!-- Bus, Mail, Livechat -->
<script type="text/javascript" src="/bus/static/src/js/bus.js"></script>
<script type="text/javascript" src="/bus/static/src/js/services/bus_service.js"></script>
<script type="text/javascript" src="/mail/static/src/js/chat_window.js"></script>
<script type="text/javascript" src="/mail/static/src/js/thread.js"></script>
<script type="text/javascript" src="/mail/static/src/js/document_viewer.js"></script>
+1 -1
View File
@@ -44,9 +44,9 @@
'application': True,
'qweb': [
'static/src/xml/activity.xml',
'static/src/xml/client_action.xml',
'static/src/xml/composer.xml',
'static/src/xml/chatter.xml',
'static/src/xml/discuss.xml',
'static/src/xml/followers.xml',
'static/src/xml/systray.xml',
'static/src/xml/thread.xml',
+2 -2
View File
@@ -258,8 +258,8 @@ class MailController(http.Controller):
def needaction(self):
return request.env['res.partner'].get_needaction_count()
@http.route('/mail/client_action', type='json', auth='user')
def mail_client_action(self):
@http.route('/mail/init_messaging', type='json', auth='user')
def mail_init_messaging(self):
values = {
'needaction_inbox_counter': request.env['res.partner'].get_needaction_count(),
'starred_counter': request.env['res.partner'].get_starred_count(),
-11
View File
@@ -186,17 +186,6 @@ class Message(models.Model):
return ids
@api.multi
def mark_as_unread(self, channel_ids=None):
""" Add needactions to messages for the current partner. """
partner_id = self.env.user.partner_id.id
for message in self:
message.write({'needaction_partner_ids': [(4, partner_id)]})
ids = [m.id for m in self]
notification = {'type': 'mark_as_unread', 'message_ids': ids, 'channel_ids': channel_ids}
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), notification)
@api.multi
def set_message_done(self):
""" Remove the needaction from messages for the current partner. """
File diff suppressed because it is too large Load Diff
-130
View File
@@ -1,130 +0,0 @@
odoo.define('mail.chat_mixin', function() {
"use strict";
var ChatMixin = {
// return a deferred that resolves when chat manager is ready
_chatReady: function() {
var result;
this.trigger_up('chat_manager_ready', {
callback: function(def) { result = def;}
});
return $.when(result);
},
_getBus: function() {
var result;
this.trigger_up('get_bus', {
callback: function(bus) { result = bus;}
});
return result;
},
_getCannedResponses: function() {
var result;
this.trigger_up('get_canned_responses', {
callback: function(responses) { result = responses;}
});
return result;
},
_getEmojis: function() {
var result;
this.trigger_up('get_emojis', {
callback: function(emojis) { result = emojis;}
});
return result;
},
_getMentionPartnerSuggestions: function(channel_id) {
var result;
this.trigger_up('get_mention_partner_suggestions', {
channel_id: channel_id,
callback: function(res) { result = res; },
});
return result;
},
_getMessages: function(options) {
var result;
this.trigger_up('get_messages', {
options: options,
callback: function(messages) { result = messages;}
});
return result ? result : $.when([]);
},
_joinChannel: function(channel_id) {
var result;
this.trigger_up('join_channel', {
channel_id: channel_id,
callback: function(def) { result = def; },
});
return $.when(result);
},
_postMessage: function(message, options) {
var result;
this.trigger_up('post_message', {
message: message,
options: options,
callback: function(def) { result = def;}
});
return $.when(result);
},
_removeChatterMessages: function(model) {
this.trigger_up('remove_chatter_messages', {
model: model,
});
},
_toggleStarStatus: function(message_id) {
this.trigger_up('toggle_star_status', {
message_id: message_id,
});
},
};
return ChatMixin;
});
odoo.define('mail.chat_service', function (require) {
"use strict";
var chat_manager = require('mail.chat_manager');
var web_client = require('web.web_client');
web_client.on('get_emojis', web_client, function(event) {
event.data.callback(chat_manager.get_emojis());
});
web_client.on('get_canned_responses', web_client, function(event) {
event.data.callback(chat_manager.get_canned_responses());
});
web_client.on('chat_manager_ready', web_client, function(event) {
event.data.callback(chat_manager.is_ready);
});
web_client.on('get_messages', web_client, function(event) {
event.data.callback(chat_manager.get_messages(event.data.options));
});
web_client.on('post_message', web_client, function(event) {
event.data.callback(chat_manager.post_message(event.data.message, event.data.options));
});
web_client.on('get_bus', web_client, function(event) {
event.data.callback(chat_manager.bus);
});
web_client.on('remove_chatter_messages', web_client, function(event) {
chat_manager.remove_chatter_messages(event.data.model);
});
web_client.on('toggle_star_status', web_client, function(event) {
chat_manager.toggle_star_status(event.data.message_id);
});
web_client.on('join_channel', web_client, function(event) {
event.data.callback(chat_manager.join_channel(event.data.channel_id));
});
web_client.on('get_mention_partner_suggestions', web_client, function(event) {
event.data.callback(chat_manager.get_mention_partner_suggestions(event.data.channel_id));
});
});
+3 -1
View File
@@ -140,7 +140,9 @@ return Widget.extend({
};
this.$input.val('');
if (content) {
this.trigger('post_message', message, this.channel_id);
this.trigger('post_message', message, {
channelID: this.channel_id,
});
}
}
},
+2 -3
View File
@@ -2,7 +2,6 @@ odoo.define('mail.Chatter', function (require) {
"use strict";
var Activity = require('mail.Activity');
var chat_mixin = require('mail.chat_mixin');
var ChatterComposer = require('mail.ChatterComposer');
var Followers = require('mail.Followers');
var ThreadField = require('mail.ThreadField');
@@ -22,7 +21,7 @@ var QWeb = core.qweb;
// with each other.
// It synchronizes the rendering of those widgets (as they may be asynchronous), to limitate
// the flickering when switching between records
var Chatter = Widget.extend(chat_mixin, {
var Chatter = Widget.extend({
template: 'mail.Chatter',
custom_events: {
reload_mail_fields: '_onReloadMailFields',
@@ -52,7 +51,7 @@ var Chatter = Widget.extend(chat_mixin, {
// mention: get the prefetched partners and use them as mention suggestions
// if there is a follower widget, the followers will be added to the
// suggestions as well once fetched
this.mentionPartnerSuggestions = this._getMentionPartnerSuggestions();
this.mentionPartnerSuggestions = this.call('chat_manager', 'getMentionPartnerSuggestions');
this.mentionSuggestions = this.mentionPartnerSuggestions;
this.fields = {};
@@ -1,7 +1,6 @@
odoo.define('mail.ChatterComposer', function (require) {
"use strict";
// var chat_manager = require('mail.chat_manager');
var composer = require('mail.composer');
var utils = require('mail.utils');
+7 -8
View File
@@ -1,7 +1,6 @@
odoo.define('mail.composer', function (require) {
"use strict";
var chat_mixin = require('mail.chat_mixin');
var DocumentViewer = require('mail.DocumentViewer');
var utils = require('mail.utils');
@@ -385,7 +384,7 @@ var MentionManager = Widget.extend({
});
var BasicComposer = Widget.extend(chat_mixin, {
var BasicComposer = Widget.extend({
template: "mail.ChatComposer",
events: {
"keydown .o_composer_input": "_onKeydown",
@@ -482,7 +481,7 @@ var BasicComposer = Widget.extend(chat_mixin, {
self.trigger('input_focused');
});
this.$input.html(this.options.default_body);
this.$input.css('min-height', this.options.input_min_height)
this.$input.css('min-height', this.options.input_min_height);
// Attachments
$(window).on(this.fileupload_id, this.on_attachment_loaded);
@@ -728,7 +727,7 @@ var BasicComposer = Widget.extend(chat_mixin, {
},
// remove mention when user try to edit or remove it.
_removeMention: function (event) {
if (window.getSelection().anchorNode.parentNode.tagName == 'A') {
if (window.getSelection().anchorNode.parentNode.tagName === 'A') {
document.getElementById(window.getSelection().anchorNode.parentNode.id).remove();
}
},
@@ -790,8 +789,8 @@ var BasicComposer = Widget.extend(chat_mixin, {
var self = this;
var def = $.Deferred();
clearTimeout(this.canned_timeout);
this.canned_timeout = setTimeout(function() {
var canned_responses = self._getCannedResponses();
this.canned_timeout = setTimeout(function () {
var canned_responses = self.call('chat_manager', 'getCannedResponses');
var matches = fuzzy.filter(utils.unaccent(search), _.pluck(canned_responses, 'source'));
var indexes = _.pluck(matches.slice(0, self.options.mention_fetch_limit), 'index');
def.resolve(_.map(indexes, function (i) {
@@ -876,7 +875,7 @@ var BasicComposer = Widget.extend(chat_mixin, {
_onEmojiButtonClick: function () {
if (!this.$emojisContainer) { // lazy rendering
this.$emojisContainer = $(QWeb.render('mail.ChatComposer.emojis', {
emojis: this._getEmojis(),
emojis: this.call('chat_manager', 'getEmojis'),
}));
}
if (this.$emojisContainer.parent().length) {
@@ -938,7 +937,7 @@ var BasicComposer = Widget.extend(chat_mixin, {
*/
_onEmojiImageClick: function (ev) {
this.$input.html(this.$input.html() + " " + $(ev.currentTarget).data('emoji') + " ");
this._placeCaretAtEnd(this.$input[0])
this._placeCaretAtEnd(this.$input[0]);
this._hideEmojis();
},
});
@@ -1,10 +1,8 @@
odoo.define('mail.chat_client_action', function (require) {
odoo.define('mail.chat_discuss', function (require) {
"use strict";
var chat_manager = require('mail.chat_manager');
var ChatThread = require('mail.ChatThread');
var composer = require('mail.composer');
var utils = require('mail.utils');
var config = require('web.config');
var ControlPanelMixin = require('web.ControlPanelMixin');
@@ -33,9 +31,11 @@ var PartnerInviteDialog = Dialog.extend({
/**
* @override
* @param {integer|string} channelID id of the channel,
* a string for static channels (e.g. 'channel_inbox').
*/
init: function (parent, title, channel_id) {
this.channel_id = channel_id;
init: function (parent, title, channelID) {
this.channelID = channelID;
this._super(parent, {
title: title,
@@ -51,7 +51,8 @@ var PartnerInviteDialog = Dialog.extend({
/**
* @override
*/
start: function (){
start: function () {
var self = this;
this.$input = this.$('.o_mail_chat_partner_invite_input');
this.$input.select2({
width: '100%',
@@ -62,7 +63,7 @@ var PartnerInviteDialog = Dialog.extend({
return $('<span>').text(item.text).prepend(status);
},
query: function (query) {
chat_manager.search_partner(query.term, 20).then(function (partners) {
self.call('chat_manager', 'searchPartner', query.term, 20).then(function (partners) {
query.callback({
results: _.map(partners, function (partner) {
return _.extend(partner, { text: partner.label });
@@ -80,7 +81,7 @@ var PartnerInviteDialog = Dialog.extend({
/**
* @private
* @returns {Deferred}
* @returns {$.Promise}
*/
_addChannel: function () {
var self = this;
@@ -89,22 +90,23 @@ var PartnerInviteDialog = Dialog.extend({
return this._rpc({
model: 'mail.channel',
method: 'channel_invite',
args: [this.channel_id],
args: [this.channelID],
kwargs: {partner_ids: _.pluck(data, 'id')},
}).then(function () {
var names = _.escape(_.pluck(data, 'text').join(', '));
var notification = _.str.sprintf(_t('You added <b>%s</b> to the conversation.'), names);
self.do_notify(_t('New people'), notification);
// Clear the members_deferred to fetch again the partner
// when get_mention_partner_suggestions from the chat_manager is triggered
delete chat_manager.get_channel(self.channel_id).members_deferred;
// Clear the membersDeferred to fetch again the partner
// when getMentionPartnerSuggestions from the chatManager is triggered
var channel = this.call('chat_manager', 'getChannel', self.channelID);
delete channel.membersDeferred;
});
}
},
});
var ChatAction = Widget.extend(ControlPanelMixin, {
template: 'mail.client_action',
var Discuss = Widget.extend(ControlPanelMixin, {
template: 'mail.discuss',
custom_events: {
search: '_onSearch',
},
@@ -129,7 +131,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
this.domain = [];
this.action = action;
this.options = options || {};
this.channels_scrolltop = {};
this.channelsScrolltop = {};
this.throttledUpdateChannels = _.throttle(this._updateChannels.bind(this), 100, { leading: false });
this.notification_bar = (window.Notification && window.Notification.permission === "default");
this.selected_message = null;
@@ -144,13 +146,14 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
*/
willStart: function () {
var self = this;
var view_id = this.action && this.action.search_view_id && this.action.search_view_id[0];
var viewID = this.action && this.action.search_view_id && this.action.search_view_id[0];
var def = this
.loadFieldView(this.dataset, view_id, 'search')
.loadFieldView(this.dataset, viewID, 'search')
.then(function (fields_view) {
self.fields_view = fields_view;
});
return $.when(this._super(), chat_manager.is_ready, def);
var chatReady = this.call('chat_manager', 'isReady');
return $.when(this._super(), chatReady, def);
},
/**
* @override
@@ -158,30 +161,31 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
start: function () {
var self = this;
var default_channel = chat_manager.get_channel(this.defaultChannelID) ||
chat_manager.get_channel('channel_inbox');
this.basic_composer = new composer.BasicComposer(this, {mention_partners_restricted: true});
this.extended_composer = new composer.ExtendedComposer(this, {mention_partners_restricted: true});
this.basic_composer.on('post_message', this, this._onPostMessage);
this.basic_composer.on('input_focused', this, this._onComposerFocused);
this.extended_composer.on('post_message', this, this._onPostMessage);
this.extended_composer.on('input_focused', this, this._onComposerFocused);
var defaultChannel = this.call('chat_manager', 'getChannel', this.defaultChannelID) ||
this.call('chat_manager', 'getChannel', 'channel_inbox');
this.basicComposer = new composer.BasicComposer(this, {mention_partners_restricted: true});
this.extendedComposer = new composer.ExtendedComposer(this, {mention_partners_restricted: true});
this.basicComposer.on('post_message', this, this._onPostMessage);
this.basicComposer.on('input_focused', this, this._onComposerFocused);
this.extendedComposer.on('post_message', this, this._onPostMessage);
this.extendedComposer.on('input_focused', this, this._onComposerFocused);
var defs = [];
defs.push(this._renderButtons());
defs.push(this._renderThread());
defs.push(this.basic_composer.appendTo(this.$('.o_mail_chat_content')));
defs.push(this.extended_composer.appendTo(this.$('.o_mail_chat_content')));
defs.push(this.basicComposer.appendTo(this.$('.o_mail_chat_content')));
defs.push(this.extendedComposer.appendTo(this.$('.o_mail_chat_content')));
defs.push(this._renderSearchView());
return $.when.apply($, defs)
.then(this._setChannel.bind(this, default_channel))
.then(this._setChannel.bind(this, defaultChannel))
.then(this._updateChannels.bind(this))
.then(function () {
self._startListening();
self.thread.$el.on("scroll", null, _.debounce(function () {
if (self.thread.is_at_bottom()) {
chat_manager.mark_channel_as_seen(self.channel);
self.call('chat_manager', 'markChannelAsSeen', self.channel);
}
}, 100));
});
@@ -210,17 +214,19 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
* @override
*/
on_attach_callback: function () {
chat_manager.bus.trigger('client_action_open', true);
var chatBus = this.call('chat_manager', 'getChatBus');
chatBus.trigger('discuss_open', true);
if (this.channel) {
this.thread.scroll_to({offset: this.channels_scrolltop[this.channel.id]});
this.thread.scroll_to({offset: this.channelsScrolltop[this.channel.id]});
}
},
/**
* @override
*/
on_detach_callback: function () {
chat_manager.bus.trigger('client_action_open', false);
this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop();
var chatBus = this.call('chat_manager', 'getChatBus');
chatBus.trigger('discuss_open', false);
this.channelsScrolltop[this.channel.id] = this.thread.get_scrolltop();
},
//--------------------------------------------------------------------------
@@ -229,14 +235,17 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
/**
* @private
* @returns {Deferred}
* @returns {$.Promise}
*/
_fetchAndRenderThread: function () {
var self = this;
return chat_manager.get_messages({channel_id: this.channel.id, domain: this.domain}).then(function (result) {
self.thread.render(result, self._getThreadRenderingOptions(result));
self._updateButtonStatus(result.length === 0);
});
return this.call('chat_manager', 'getMessages', {
channelID: this.channel.id,
domain: this.domain
}).then(function (messages) {
self.thread.render(messages, self._getThreadRenderingOptions(messages));
self._updateButtonStatus(messages.length === 0);
});
},
/**
* @private
@@ -250,13 +259,13 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
if (!this.unread_counter) {
this.messages_separator_position = false; // no unread message -> don't display separator
} else {
var msg = chat_manager.get_last_seen_message(this.channel);
var msg = this.call('chat_manager', 'getLastSeenMessage', this.channel);
this.messages_separator_position = msg ? msg.id : 'top';
}
}
return {
channel_id: this.channel.id,
display_load_more: !chat_manager.all_history_loaded(this.channel, this.domain),
display_load_more: !this.call('chat_manager', 'isAllHistoryLoaded', this.channel, this.domain),
display_needactions: this.channel.display_needactions,
messages_separator_position: this.messages_separator_position,
squash_close_messages: this.channel.type !== 'static' && !this.channel.mass_mailing,
@@ -268,22 +277,27 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
};
},
/**
* Load more messages for the current thread
*
* @private
* @returns {Deferred}
* @returns {$.Promise}
*/
_loadMoreMessages: function () {
var self = this;
var oldest_msg_id = this.$('.o_thread_message').first().data('messageId');
var oldest_msg_selector = '.o_thread_message[data-message-id="' + oldest_msg_id + '"]';
var offset = -dom.getPosition(document.querySelector(oldest_msg_selector)).top;
return chat_manager
.get_messages({channel_id: this.channel.id, domain: this.domain, load_more: true})
.then(function (result) {
var oldestMsgID = this.$('.o_thread_message').first().data('messageId');
var oldestMsgSelector = '.o_thread_message[data-message-id="' + oldestMsgID + '"]';
var offset = -dom.getPosition(document.querySelector(oldestMsgSelector)).top;
return this.call('chat_manager', 'getMessages', {
channelID: this.channel.id,
domain: this.domain,
loadMore: true
})
.then(function (messages) {
if (self.messages_separator_position === 'top') {
self.messages_separator_position = undefined; // reset value to re-compute separator position
}
self.thread.render(result, self._getThreadRenderingOptions(result));
offset += dom.getPosition(document.querySelector(oldest_msg_selector)).top;
self.thread.render(messages, self._getThreadRenderingOptions(messages));
offset += dom.getPosition(document.querySelector(oldestMsgSelector)).top;
self.thread.scroll_to({offset: offset});
});
},
@@ -313,9 +327,9 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
select: function (event, ui) {
if (self.last_search_val) {
if (ui.item.value === '_create') {
chat_manager.create_channel(self.last_search_val, "public");
self.call('chat_manager', 'createChannel', self.last_search_val, "public");
} else {
chat_manager.join_channel(ui.item.id);
self.call('chat_manager', 'joinChannel', ui.item.id);
}
}
},
@@ -327,23 +341,23 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
} else if (type === 'private') {
$input.on('keyup', this, function (event) {
var name = _.escape($(event.target).val());
if(event.which === $.ui.keyCode.ENTER && name) {
chat_manager.create_channel(name, "private");
if (event.which === $.ui.keyCode.ENTER && name) {
self.call('chat_manager', 'createChannel', name, "private");
}
});
} else if (type === 'dm') {
$input.autocomplete({
source: function (request, response) {
self.last_search_val = _.escape(request.term);
chat_manager.search_partner(self.last_search_val, 10).done(response);
self.call('chat_manager', 'searchPartner', self.last_search_val, 10).done(response);
},
select: function (event, ui) {
var partner_id = ui.item.id;
var dm = chat_manager.get_dm_from_partner_id(partner_id);
var dm = self.call('chat_manager', 'getDmFromPartnerID', partner_id);
if (dm) {
self._setChannel(dm);
} else {
chat_manager.create_channel(partner_id, "dm");
self.call('chat_manager', 'createChannel', partner_id, "dm");
}
// clear the input
$(this).val('');
@@ -418,20 +432,21 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
* @returns {Deferred}
*/
_renderThread: function () {
var self = this;
this.thread = new ChatThread(this, {display_help: true});
this.thread.on('redirect', this, function (res_model, res_id) {
chat_manager.redirect(res_model, res_id, this._setChannel.bind(this));
this.thread.on('redirect', this, function (resModel, resID) {
self.call('chat_manager', 'redirect', resModel, resID, self._setChannel.bind(self));
});
this.thread.on('redirect_to_channel', this, function (channel_id) {
chat_manager.join_channel(channel_id).then(this._setChannel.bind(this));
this.thread.on('redirect_to_channel', this, function (channelID) {
self.call('chat_manager', 'joinChannel', channelID).then(this._setChannel.bind(this));
});
this.thread.on('load_more_messages', this, this._loadMoreMessages);
this.thread.on('mark_as_read', this, function (message_id) {
chat_manager.mark_as_read([message_id]);
this.thread.on('mark_as_read', this, function (messageID) {
self.call('chat_manager', 'markAsRead', [messageID]);
});
this.thread.on('toggle_star_status', this, function (message_id) {
chat_manager.toggle_star_status(message_id);
this.thread.on('toggle_star_status', this, function (messageID) {
self.call('chat_manager', 'toggleStarStatus', messageID);
});
this.thread.on('select_message', this, this._selectMessage);
this.thread.on('unselect_message', this, this._unselectMessage);
@@ -444,12 +459,12 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
* @private
*/
_restoreChannelState: function () {
var $new_messages_separator = this.$('.o_thread_new_messages_separator');
if ($new_messages_separator.length) {
this.thread.$el.scrollTo($new_messages_separator);
var $newMessagesSeparator = this.$('.o_thread_new_messages_separator');
if ($newMessagesSeparator.length) {
this.thread.$el.scrollTo($newMessagesSeparator);
} else {
var new_channel_scrolltop = this.channels_scrolltop[this.channel.id];
this.thread.scroll_to({offset: new_channel_scrolltop});
var newChannelScrolltop = this.channelsScrolltop[this.channel.id];
this.thread.scroll_to({offset: newChannelScrolltop});
}
this._restoreComposerState(this.channel);
},
@@ -461,7 +476,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
if (channel.type === 'static') {
return; // no composer in static channels
}
var composer = channel.mass_mailing ? this.extended_composer : this.basic_composer;
var composer = channel.mass_mailing ? this.extendedComposer : this.basicComposer;
var composerState = this.composerStates[channel.uuid];
if (composerState) {
composer.setState(composerState);
@@ -469,14 +484,14 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
},
/**
* @private
* @param {string} search_val
* @returns {Deferred<Array>}
* @param {string} searchVal
* @returns {$.Promise<Array>}
*/
_searchChannel: function (search_val){
_searchChannel: function (searchVal){
return this._rpc({
model: 'mail.channel',
method: 'channel_search_to_join',
args: [search_val]
args: [searchVal]
})
.then(function (result){
var values = [];
@@ -492,27 +507,27 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
},
/**
* @private
* @param {integer} message_id
* @param {integer} messageID
*/
_selectMessage: function (message_id) {
_selectMessage: function (messageID) {
this.$el.addClass('o_mail_selection_mode');
var message = chat_manager.get_message(message_id);
var message = this._getMessage(messageID);
this.selected_message = message;
var subject = "Re: " + message.record_name;
this.extended_composer.set_subject(subject);
this.extendedComposer.set_subject(subject);
if (this.channel.type !== 'static') {
this.basic_composer.do_hide();
this.basicComposer.do_hide();
}
this.extended_composer.do_show();
this.extendedComposer.do_show();
this.thread.scroll_to({id: message_id, duration: 200, only_if_necessary: true});
this.extended_composer.focus('body');
this.thread.scroll_to({id: messageID, duration: 200, only_if_necessary: true});
this.extendedComposer.focus('body');
},
/**
* @private
* @param {Object} channel
* @returns {Deferred}
* @returns {$.Promise}
*/
_setChannel: function (channel) {
var self = this;
@@ -534,9 +549,8 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
return this._fetchAndRenderThread().then(function () {
// Mark channel's messages as read and clear needactions
if (channel.type !== 'static') {
chat_manager.mark_channel_as_seen(channel);
self.call('chat_manager', 'markChannelAsSeen', channel);
}
// Restore scroll position and composer of the new current channel
self._restoreChannelState();
@@ -556,25 +570,26 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
});
},
/**
* Binds handlers on chat_manager events
* Binds handlers on chatManager events
*
* @private
*/
_startListening: function () {
chat_manager.bus.on('open_channel', this, this._setChannel);
chat_manager.bus.on('new_message', this, this._onNewMessage);
chat_manager.bus.on('update_message', this, this._onMessageUpdated);
chat_manager.bus.on('new_channel', this, this._onNewChannel);
chat_manager.bus.on('anyone_listening', this, function (channel, query) {
var chatBus = this.call('chat_manager', 'getChatBus');
chatBus.on('open_channel', this, this._setChannel);
chatBus.on('new_message', this, this._onNewMessage);
chatBus.on('update_message', this, this._onMessageUpdated);
chatBus.on('new_channel', this, this._onNewChannel);
chatBus.on('anyone_listening', this, function (channel, query) {
query.is_displayed = query.is_displayed ||
(channel.id === this.channel.id && this.thread.is_at_bottom());
});
chat_manager.bus.on('unsubscribe_from_channel', this, this._onChannelLeft);
chat_manager.bus.on('update_needaction', this, this.throttledUpdateChannels);
chat_manager.bus.on('update_starred', this, this.throttledUpdateChannels);
chat_manager.bus.on('update_channel_unread_counter', this, this.throttledUpdateChannels);
chat_manager.bus.on('update_dm_presence', this, this.throttledUpdateChannels);
chat_manager.bus.on('activity_updated', this, this.throttledUpdateChannels);
chatBus.on('unsubscribe_from_channel', this, this._onChannelLeft);
chatBus.on('update_needaction', this, this.throttledUpdateChannels);
chatBus.on('update_starred', this, this.throttledUpdateChannels);
chatBus.on('update_channel_unread_counter', this, this.throttledUpdateChannels);
chatBus.on('update_dm_presence', this, this.throttledUpdateChannels);
chatBus.on('activity_updated', this, this.throttledUpdateChannels);
},
/**
* Stores the scroll position and composer state of the current channel
@@ -583,7 +598,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
*/
_storeChannelState: function () {
if (this.channel) {
this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop();
this.channelsScrolltop[this.channel.id] = this.thread.get_scrolltop();
this._storeComposerState(this.channel);
}
},
@@ -595,7 +610,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
if (channel.type === 'static') {
return; // no composer in static channels
}
var composer = channel.mass_mailing ? this.extended_composer : this.basic_composer;
var composer = channel.mass_mailing ? this.extendedComposer : this.basicComposer;
this.composerStates[channel.uuid] = composer.getState();
composer.clear_composer();
},
@@ -603,11 +618,11 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
* @private
*/
_unselectMessage: function () {
this.basic_composer.do_toggle(this.channel.type !== 'static' && !this.channel.mass_mailing);
this.extended_composer.do_toggle(this.channel.type !== 'static' && this.channel.mass_mailing);
this.basicComposer.do_toggle(this.channel.type !== 'static' && !this.channel.mass_mailing);
this.extendedComposer.do_toggle(this.channel.type !== 'static' && this.channel.mass_mailing);
if (!config.device.touch) {
var composer = this.channel.mass_mailing ? this.extended_composer : this.basic_composer;
var composer = this.channel.mass_mailing ? this.extendedComposer : this.basicComposer;
composer.focus();
}
this.$el.removeClass('o_mail_selection_mode');
@@ -623,11 +638,11 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
var self = this;
var $sidebar = this._renderSidebar({
active_channel_id: this.channel ? this.channel.id: undefined,
channels: chat_manager.get_channels(),
needaction_counter: chat_manager.get_needaction_counter(),
starred_counter: chat_manager.get_starred_counter(),
channels: this.call('chat_manager', 'getChannels'),
needaction_counter: this.call('chat_manager', 'getNeedactionCounter'),
starred_counter: this.call('chat_manager', 'getStarredCounter'),
});
this.$(".o_mail_chat_sidebar").html($sidebar.contents());
self.$(".o_mail_chat_sidebar").html($sidebar.contents());
_.each(['dm', 'public', 'private'], function (type) {
var $input = self.$('.o_mail_add_channel[data-type=' + type + '] input');
self._prepareAddChannelInput($input, type);
@@ -729,27 +744,29 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
*/
_onChannelClicked: function (event) {
event.preventDefault();
var channel_id = $(event.currentTarget).data('channel-id');
this._setChannel(chat_manager.get_channel(channel_id));
var channelID = $(event.currentTarget).data('channel-id');
var channel = this.call('chat_manager', 'getChannel', channelID);
this._setChannel(channel);
},
/**
* @private
* @param {integer || string} channel_id
* @param {integer|string} channelID
*/
_onChannelLeft: function (channel_id) {
if (this.channel.id === channel_id) {
this._setChannel(chat_manager.get_channel("channel_inbox"));
_onChannelLeft: function (channelID) {
if (this.channel.id === channelID) {
var channel = this.call('chat_manager', 'getChannel', 'channel_inbox');
this._setChannel(channel);
}
this._updateChannels();
delete this.channels_scrolltop[channel_id];
delete this.channelsScrolltop[channelID];
},
/**
* @private
*/
_onComposerFocused: function () {
var composer = this.channel.mass_mailing ? this.extended_composer : this.basic_composer;
var commands = chat_manager.get_commands(this.channel);
var partners = chat_manager.get_mention_partner_suggestions(this.channel);
var composer = this.channel.mass_mailing ? this.extendedComposer : this.basicComposer;
var commands = this.call('chat_manager', 'getCommands', this.channel);
var partners = this.call('chat_manager', 'getMentionPartnerSuggestions', this.channel);
composer.mention_set_enabled_commands(commands);
composer.mention_set_prefetched_partners(partners);
},
@@ -757,7 +774,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
* @private
*/
_onMarkAllReadClicked: function () {
chat_manager.mark_all_as_read(this.channel, this.domain);
this.call('chat_manager', 'markAllAsRead', this.channel, this.domain);
},
/**
* @private
@@ -766,16 +783,20 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
*/
_onMessageUpdated: function (message, type) {
var self = this;
var current_channel_id = this.channel.id;
if ((current_channel_id === "channel_starred" && !message.is_starred) ||
(current_channel_id === "channel_inbox" && !message.is_needaction)) {
chat_manager.get_messages({channel_id: this.channel.id, domain: this.domain}).then(function (messages) {
var options = self._getThreadRenderingOptions(messages);
self.thread.remove_message_and_render(message.id, messages, options).then(function () {
self._updateButtonStatus(messages.length === 0, type);
var currentChannelID = this.channel.id;
if ((currentChannelID === "channel_starred" && !message.is_starred) ||
(currentChannelID === "channel_inbox" && !message.is_needaction)) {
this.call('chat_manager', 'getMessages', {
channelID: this.channel.id,
domain: this.domain
}).then(function (messages) {
var options = self._getThreadRenderingOptions(messages);
self.thread.remove_message_and_render(message.id, messages, options).then(function () {
self._updateButtonStatus(messages.length === 0, type);
});
});
});
} else if (_.contains(message.channel_ids, current_channel_id)) {
} else if (_.contains(message.channel_ids, currentChannelID)) {
this._fetchAndRenderThread();
}
},
@@ -797,9 +818,8 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
var self = this;
if (_.contains(message.channel_ids, this.channel.id)) {
if (this.channel.type !== 'static' && this.thread.is_at_bottom()) {
chat_manager.mark_channel_as_seen(this.channel);
this.call('chat_manager', 'markChannelAsSeen', this.channel);
}
var should_scroll = this.thread.is_at_bottom();
this._fetchAndRenderThread().then(function () {
if (should_scroll) {
@@ -810,7 +830,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
// Re-render sidebar to indicate that there is a new message in the corresponding channels
this._updateChannels();
// Dump scroll position of channels in which the new message arrived
this.channels_scrolltop = _.omit(this.channels_scrolltop, message.channel_ids);
this.channelsScrolltop = _.omit(this.channelsScrolltop, message.channel_ids);
},
/**
* @private
@@ -818,18 +838,14 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
*/
_onPostMessage: function (message) {
var self = this;
var options = this.selected_message ? {} : {channel_id: this.channel.id};
var options = this.selected_message ? {} : {channelID: this.channel.id};
if (this.selected_message) {
message.subtype = this.selected_message.is_note ? 'mail.mt_note': 'mail.mt_comment';
message.subtype_id = false;
message.message_type = 'comment';
message.content_subtype = 'html';
options.model = this.selected_message.model;
options.res_id = this.selected_message.res_id;
}
chat_manager
.post_message(message, options)
this.call('chat_manager', 'postMessage', message, options)
.then(function () {
if (self.selected_message) {
self._renderSnackbar('mail.chat.MessageSentSnackbar', {record_name: self.selected_message.record_name}, 5000);
@@ -890,10 +906,10 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
if (def) {
def.then(function (value) {
if (value === 'denied') {
utils.send_notification(self, _t('Permission denied'),
self._sendNotification(self, _t('Permission denied'),
_t('Odoo will not have the permission to send native notifications on this device.'));
} else {
utils.send_notification(self, _t('Permission granted'),
self._sendNotification(self, _t('Permission granted'),
_t('Odoo has now the permission to send you native notifications on this device.'));
}
});
@@ -936,25 +952,26 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
*/
_onUnpinChannel: function (event) {
event.stopPropagation();
var channel_id = $(event.target).data("channel-id");
chat_manager.unsubscribe(chat_manager.get_channel(channel_id));
var channelID = $(event.target).data("channel-id");
var channel = this.call('chat_manager', 'getChannel', channelID);
this.call('chat_manager', 'unsubscribe', channel);
},
/**
* @private
*/
_onUnstarAllClicked: function () {
chat_manager.unstar_all();
this.call('chat_manager', 'unstarAll');
},
/**
* @private
*/
_onUnsubscribeButtonClicked: function () {
chat_manager.unsubscribe(this.channel);
this._unsubscribe(this.channel);
},
});
core.action_registry.add('mail.chat.instant_messaging', ChatAction);
core.action_registry.add('mail.chat.instant_messaging', Discuss);
return ChatAction;
return Discuss;
});
@@ -1,12 +1,10 @@
odoo.define('mail.chat_client_action_mobile', function (require) {
odoo.define('mail.chat_discuss_mobile', function (require) {
"use strict";
var ChatAction = require('mail.chat_client_action');
var chat_manager = require('mail.chat_manager');
var Discuss = require('mail.chat_discuss');
var config = require('web.config');
var core = require('web.core');
var session = require('web.session');
var QWeb = core.qweb;
@@ -14,10 +12,10 @@ if (!config.device.isMobile) {
return;
}
ChatAction.include({
template: 'mail.client_action_mobile',
Discuss.include({
template: 'mail.discuss_mobile',
need_control_panel: false, // in mobile, we use a custom control panel
events: _.extend(ChatAction.prototype.events, {
events: _.extend(Discuss.prototype.events, {
'click .o_mail_mobile_tab': '_onMobileTabClicked',
'click .o_channel_inbox_item': '_onMobileInboxButtonClicked',
'click .o_mail_channel_preview': '_onMobileChannelClicked',
@@ -103,10 +101,11 @@ ChatAction.include({
/**
* @override
* @private
* @param {Object} channel
*/
_setChannel: function (channel) {
if (channel.type !== 'static') {
chat_manager.detach_channel(channel);
this.call('chat_manager', 'detachChannel', channel.id);
} else {
this._super.apply(this, arguments);
}
@@ -170,8 +169,9 @@ ChatAction.include({
if (inInbox) {
def = this._fetchAndRenderThread();
} else {
var channels = _.where(chat_manager.get_channels(), {type: type});
def = chat_manager.get_channels_preview(channels);
var allChannels = this.call('chat_manager', 'getChannels');
var channels = _.where(allChannels, {type: type});
def = this.call('chat_manager', 'getChannelsPreview', channels);
}
return $.when(def).then(function (channelsPreview) {
// update content
@@ -179,19 +179,15 @@ ChatAction.include({
if (!previouslyInInbox) {
self.$('.o_mail_chat_tab_pane').remove();
self.$mainContent.append(self.thread.$el);
self.$mainContent.append(self.extended_composer.$el);
self.$mainContent.append(self.extendedComposer.$el);
}
self._restoreChannelState();
} else {
self.thread.$el.detach();
self.extended_composer.$el.detach();
self.extendedComposer.$el.detach();
var $content = $(QWeb.render("mail.chat.MobileTabPane", {
channels: channelsPreview,
get_message_body_preview: chat_manager.get_message_body_preview,
moment: moment,
partner_id: session.partner_id,
type: type,
widget: self,
}));
self._prepareAddChannelInput($content.find('.o_mail_add_channel input'), type);
self.$mainContent.html($content);
@@ -252,7 +248,8 @@ ChatAction.include({
* @param {MouseEvent}
*/
_onMobileInboxButtonClicked: function (event) {
this._setChannel(chat_manager.get_channel($(event.currentTarget).data('type')));
var channel = this.call('chat_manager', 'getChannel', $(event.currentTarget).data('type'));
this._setChannel(channel);
this._updateContent(this.channel.id);
},
/**
@@ -264,7 +261,8 @@ ChatAction.include({
_onMobileTabClicked: function (event) {
var type = $(event.currentTarget).data('type');
if (type === 'channel_inbox') {
this._setChannel(chat_manager.get_channel('channel_inbox'));
var channel = this.call('chat_manager', 'getChannel', 'channel_inbox');
this._setChannel(channel);
}
this._updateContent(type);
},
@@ -275,8 +273,8 @@ ChatAction.include({
* @param {MouseEvent}
*/
_onMobileChannelClicked: function (event) {
var channelId = $(event.currentTarget).data("channel_id");
chat_manager.detach_channel(chat_manager.get_channel(channelId));
var channelID = $(event.currentTarget).data("channel_id");
this.call('chat_manager', 'detachChannel', channelID);
},
});
@@ -1,9 +1,6 @@
odoo.define('mail.ExtendedChatWindow', function (require) {
"use strict";
var core = require('web.core');
var chat_manager = require('mail.chat_manager');
var ChatWindow = require('mail.ChatWindow');
var composer = require('mail.composer');
@@ -19,27 +16,29 @@ return ChatWindow.extend({
this.$el.addClass('o_thread_less');
this.$('.o_chat_search_input input')
.autocomplete({
source: function(request, response) {
chat_manager.search_partner(request.term, 10).done(response);
source: function (request, response) {
self.call('chat_manager', 'searchPartner', request.term, 10).done(response);
},
select: function(event, ui) {
select: function (event, ui) {
self.trigger('open_dm_session', ui.item.id);
},
})
.focus();
} else if (!self.options.input_less) {
var basic_composer = new composer.BasicComposer(self, {mention_partners_restricted: true, isMini: true});
basic_composer.on('post_message', self, function (message) {
this.trigger('post_message', message, this.channel_id);
var basicComposer = new composer.BasicComposer(self, {mention_partners_restricted: true, isMini: true});
basicComposer.on('post_message', self, function (message) {
self.call('chat_manager', 'postMessage', message, {
channelID: self.channel_id,
});
});
basic_composer.once('input_focused', self, function () {
var channel = chat_manager.get_channel(this.channel_id);
var commands = chat_manager.get_commands(channel);
var partners = chat_manager.get_mention_partner_suggestions(channel);
basic_composer.mention_set_enabled_commands(commands);
basic_composer.mention_set_prefetched_partners(partners);
basicComposer.once('input_focused', self, function () {
var channel = self.call('chat_manager', 'getChannel', self.channel_id);
var commands = self.call('chat_manager', 'getCommands', channel);
var partners = self.call('chat_manager', 'getMentionPartnerSuggestions', channel);
basicComposer.mention_set_enabled_commands(commands);
basicComposer.mention_set_prefetched_partners(partners);
});
def = basic_composer.replace(self.$('.o_chat_composer'));
def = basicComposer.replace(self.$('.o_chat_composer'));
}
return $.when(this._super(), def);
},
@@ -49,7 +48,8 @@ return ChatWindow.extend({
event.stopPropagation();
},
on_reverse_breadcrumb: function () {
chat_manager.bus.trigger('client_action_open', false);
var chatBus = this.call('chat_manager', 'getChatBus');
chatBus.trigger('discuss_open', false);
},
on_click_expand: _.debounce(function (event) {
event.preventDefault();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,532 @@
odoo.define('mail.ChatWindowManager', function (require) {
"use strict";
var ExtendedChatWindow = require('mail.ExtendedChatWindow');
var AbstractService = require('web.AbstractService');
var config = require('web.config');
var core = require('web.core');
var utils = require('web.utils');
var web_client = require('web.web_client');
var _t = core._t;
var QWeb = core.qweb;
//----------------------------------------------------------------
var CHAT_WINDOW_WIDTH = 325 + 5; // 5 pixels between windows
/**
* This service handles chat channels that are displayed in "chat window" mode
*/
var ChatWindowManager = AbstractService.extend({
name: 'chat_window_manager',
dependencies: ['chat_manager'],
/**
* @override
*/
init: function () {
var self = this;
this._super.apply(this, arguments);
this.chatSessions = [];
this.newChatSession;
this.displayState = {
chatWindowsHidden: false, // chat windows aren't displayed when the client action is open
hiddenSessions: [],
hiddenUnreadCounter: 0, // total number of unread msgs in hidden chat windows
nbSlots: 0,
spaceLeft: 0,
windowsDropdownIsOpen: false, // used to keep dropdown open when closing chat windows
};
var chatReady = this.call('chat_manager', 'isReady');
chatReady.then(function () {
var channels = self.call('chat_manager', 'getChannels');
_.each(channels, function (channel) {
if (channel.is_detached) {
self.openChat(channel);
}
});
});
var chatBus = this.call('chat_manager', 'getChatBus');
chatBus.on('update_message', this, this._onUpdateMessage);
chatBus.on('new_message', this, this._onNewMessage);
chatBus.on('anyone_listening', this, this._onAnyoneListening);
chatBus.on('unsubscribe_from_channel', this, this._onUnsubscribeFromChannel);
chatBus.on('update_channel_unread_counter', this, this._onUpdateChannelUnreadCounter);
chatBus.on('update_dm_presence', this, this._onUpdateDmPresence);
chatBus.on('detach_channel', this, this._onDetachChannel);
chatBus.on('discuss_open', this, this._onDiscussOpen);
core.bus.on('resize', this, _.debounce(this._repositionWindows.bind(this), 100));
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @param {Object} chatSession
* @param {integer} chatSession.id
* @param {Object} options
*/
closeChat: function (chatSession, options) {
var session = _.find(this.chatSessions, {id: chatSession.id});
if (session) {
this._closeChat(session, options);
}
},
/**
* @param {Object} session
* @param {integer} session.id
* @param {boolean} [session.is_chat] true for dm? => server_type="chat" & type="dm"...
* @param {boolean} [session.mass_mailing]
* @param {string} [session.status] e.g. 'offline'
* @param {Object} [options]
* @param {boolean} [options.passively] if set to true, open the chat window
* without focusing the input and marking messages as read if it is not
* open yet, and do nothing otherwise
*/
openChat: function (session, options) {
var self = this;
if (!session) {
this._openChatWithoutSession();
return;
}
options = options || {};
var chatSession = _.findWhere(this.chatSessions, {id: session.id});
if (!chatSession) {
var prefix = !session.is_chat ? "#" : "";
var windowOptions = {
autofocus: !options.passively,
input_less: session.mass_mailing,
status: session.status,
};
chatSession = {
id: session.id,
uuid: session.uuid,
name: session.name,
keep_unread: options.passively, // don't automatically mark unread messages as seen
window: new ExtendedChatWindow(web_client, session.id, prefix + session.name, session.is_folded, session.unread_counter, windowOptions),
};
chatSession.window.on("close_chat_session", null, function () {
self._closeChat(chatSession);
self.call('chat_manager', 'closeChatSession', chatSession.id);
});
chatSession.window.on("toggle_star_status", null, function (messageID) {
self.call('chat_manager', 'toggleStarStatus', messageID);
});
chatSession.window.on("fold_channel", null, function (channelID, folded) {
self.call('chat_manager', 'foldChannel', channelID, folded);
});
chatSession.window.on("post_message", null, function (message, channelID) {
self.call('chat_manager', 'postMessage', message, {
channelID: channelID
}).then(function () {
chatSession.window.thread.scroll_to();
});
});
chatSession.window.on("redirect", null, function (resModel, resID) {
self.call('chat_manager', 'redirect', resModel, resID, self.openChat.bind(self));
});
chatSession.window.on("redirect_to_channel", null, function (channelID) {
var session = _.findWhere(self.chatSessions, {id: channelID});
if (!session) {
self.call('chat_manager', 'joinChannel', channelID).then(function (channel) {
self.call('chat_manager', 'detachChannel', channelID);
});
} else {
session.window.toggle_fold(false);
}
});
var removeNewChat = false;
if (options.passively) {
this.chatSessions.push(chatSession); // simply insert the window to the left
} else if (this.newChatSession && this.newChatSession.partner_id && this.newChatSession.partner_id === session.direct_partner_id) {
// the window takes the place of the 'newChatSession' window
this.chatSessions[_.indexOf(this.chatSessions, this.newChatSession)] = chatSession;
removeNewChat = true;
} else {
this._addChatSession(chatSession); // add session such that window is visible
}
chatSession.window.appendTo($('body'))
.then(function () {
self._repositionWindows({remove_new_chat: removeNewChat});
return self.call('chat_manager', 'getMessages', {channelID: chatSession.id});
}).then(function (messages) {
chatSession.window.render(messages);
chatSession.window.thread.scroll_to();
setTimeout(function () {
chatSession.window.thread.$el.on("scroll", null, _.debounce(function () {
if (!chatSession.keep_unread && chatSession.window.thread.is_at_bottom()) {
self.call('chat_manager', 'markChannelAsSeen', session);
}
}, 100));
}, 0); // setTimeout to prevent to execute handler on first scroll_to, which is asynchronous
if (options.passively) {
// mark first unread messages as seen when focusing the window, then on scroll to bottom as usual
chatSession.window.$('.o_mail_thread, .o_chat_composer').one('click', function () {
self.call('chat_manager', 'markChannelAsSeen', session);
});
} else if (!self.displayState.chatWindowsHidden && !session.is_folded) {
self.call('chat_manager', 'markChannelAsSeen', session);
}
});
} else if (!options.passively) {
if (chatSession.window.is_hidden) {
this._makeSessionVisible(chatSession);
} else if (session.is_folded !== chatSession.window.folded) {
chatSession.window.toggle_fold(session.is_folded);
}
}
},
/**
* Called when unfolding the chat window
*
* @param {Object} channel
* @param {integer} channel.id
* @param {boolean} [channel.is_folded]
*/
toggleFoldChat: function (channel) {
var session = _.find(this.chatSessions, {id: channel.id});
if (session) {
session.window.toggle_fold(channel.is_folded);
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Add chatSession such that it will be the left-most visible window
*
* @private
* @param {Object} chatSession
*/
_addChatSession: function (chatSession) {
this._computeAvailableSlots(this.chatSessions.length+1);
this.chatSessions.splice(this.displayState.nbSlots-1, 0, chatSession);
},
/**
* @private
* @param {Object} chatSession
* @param {boolean} [chatSession.keep_unread]
* @param {Object} chatSession.window
* @param {Object} [options]
* @param {boolean} [options.keep_open_if_unread]
*/
_closeChat: function (chatSession, options) {
if (options && options.keep_open_if_unread && chatSession.keep_unread) {
return;
}
this.chatSessions = _.without(this.chatSessions, chatSession);
chatSession.window.destroy();
this._repositionWindows();
},
/**
* @private
*/
_closeNewChat: function () {
this.chatSessions = _.without(this.chatSessions, this.newChatSession);
this._repositionWindows({remove_new_chat: true});
},
/**
* @private
* @param {integer} nbWindows
*/
_computeAvailableSlots: function (nbWindows) {
if (config.device.isMobile) {
this.displayState.nbSlots = 1; // one chat window full screen in mobile
return;
}
var width = window.innerWidth;
var nbSlots = Math.floor(width/CHAT_WINDOW_WIDTH);
var spaceLeft = width - (Math.min(nbSlots, nbWindows)*CHAT_WINDOW_WIDTH);
if (nbSlots < nbWindows && spaceLeft < 50) {
nbSlots--; // leave at least 50px for the hidden windows dropdown button
spaceLeft += CHAT_WINDOW_WIDTH;
}
this.displayState.nbSlots = nbSlots;
this.displayState.spaceLeft = spaceLeft;
},
/**
* @private
*/
_destroyNewChat: function () {
this.newChatSession.window.destroy();
this.newChatSession = undefined;
},
/**
* @private
* @param {Object} session
* @param {Widget} session.window
*/
_makeSessionVisible: function (session) {
utils.swap(this.chatSessions, session, this.chatSessions[this.displayState.nbSlots-1]);
this._repositionWindows();
session.window.toggle_fold(false);
},
/**
* @private
*/
_openChatWithoutSession: function () {
var self = this;
if (!this.newChatSession) {
this.newChatSession = {
id: '_open',
window: new ExtendedChatWindow(web_client, undefined, _t('New message'), false, 0, {thread_less: true}),
};
this.newChatSession.window.on("close_chat_session", null, this._closeNewChat.bind(this));
this.newChatSession.window.on('open_dm_session', null, function (partner_id) {
self.newChatSession.partner_id = partner_id;
var dm = self.call('chat_manager', 'getDmFromPartnerID', partner_id);
if (!dm) {
self.call('chat_manager', 'openAndDetachDm', partner_id);
} else {
var dmSession = _.findWhere(self.chatSessions, {id: dm.id});
if (!dmSession) {
self.call('chat_manager', 'detachChannel', dm.id);
} else {
self._closeChat(dmSession);
dm.is_folded = false;
self.openChat(dm);
}
}
});
this._addChatSession(this.newChatSession);
this.newChatSession.window.appendTo($('body')).then(this._repositionWindows.bind(this));
} else {
if (this.newChatSession.window.is_hidden) {
this._makeSessionVisible(this.newChatSession);
} else if (this.newChatSession.window.folded) {
this.newChatSession.window.toggle_fold(false);
}
}
},
/**
* @private
* @param {Object} message
* @param {integer[]} message.channel_ids
* @param {boolean} scrollBottom
*/
_updateSessions: function (message, scrollBottom) {
var self = this;
_.each(this.chatSessions, function (session) {
if (_.contains(message.channel_ids, session.id)) {
var messageVisible = !self.displayState.chatWindowsHidden && !session.window.folded &&
!session.window.is_hidden && session.window.thread.is_at_bottom();
if (messageVisible && !session.keep_unread) {
var channel = self.call('chat_manager', 'getChannel', session.id);
self.call('chat_manager', 'markChannelAsSeen', channel);
}
self.call('chat_manager', 'getMessages', {channelID: session.id}).then(function (messages) {
session.window.render(messages);
if (scrollBottom && messageVisible) {
session.window.thread.scroll_to();
}
});
}
});
},
/**
* @private
*/
_renderHiddenSessionsDropdown: function () {
var $dropdown = $(QWeb.render("mail.ChatWindowsDropdown", {
sessions: this.displayState.hiddenSessions,
open: this.displayState.windowsDropdownIsOpen,
unread_counter: this.displayState.hiddenUnreadCounter,
widget: {isMobile: config.device.isMobile},
}));
return $dropdown;
},
/**
* @private
*/
_repositionHiddenSessionsDropdown: function () {
// Unfold dropdown to the left if there is enough place
var $dropdownUL = this.displayState.$hiddenWindowsDropdown.children('ul');
if (this.displayState.spaceLeft > $dropdownUL.width() + 10) {
$dropdownUL.addClass('dropdown-menu-right');
}
},
/**
* @private
* @param {Object} [options]
* @param {boolean} [options.remove_new_chat]
*/
_repositionWindows: function (options) {
var self = this;
if (options && options.remove_new_chat) {
this._destroyNewChat();
}
if (this.displayState.chatWindowsHidden) {
return;
}
this._computeAvailableSlots(this.chatSessions.length);
var hiddenSessions = [];
var hiddenUnreadCounter = 0;
var nbSlots = this.displayState.nbSlots;
_.each(this.chatSessions, function (session, index) {
if (index < nbSlots) {
session.window.$el.css({right: CHAT_WINDOW_WIDTH*index, bottom: 0});
session.window.do_show();
} else {
hiddenSessions.push(session);
hiddenUnreadCounter += session.window.unread_msgs;
session.window.do_hide();
}
});
this.displayState.hiddenSessions = hiddenSessions;
this.displayState.hiddenUnreadCounter = hiddenUnreadCounter;
if (this.displayState.$hidden_windows_dropdown) {
this.displayState.$hidden_windows_dropdown.remove();
}
if (hiddenSessions.length) {
this.displayState.$hiddenWindowsDropdown = this._renderHiddenSessionsDropdown();
var $hiddenWindowsDropdown = this.displayState.$hiddenWindowsDropdown;
$hiddenWindowsDropdown.css({right: CHAT_WINDOW_WIDTH * nbSlots, bottom: 0})
.appendTo($('body'));
this._repositionHiddenSessionsDropdown();
this.displayState.windowsDropdownIsOpen = false;
$hiddenWindowsDropdown.on('click', '.o_chat_header', function (event) {
var sessionID = $(event.currentTarget).data('session-id');
var session = _.findWhere(hiddenSessions, {id: sessionID});
if (session) {
self._makeSessionVisible(session);
}
});
$hiddenWindowsDropdown.on('click', '.o_chat_window_close', function (event) {
var sessionID = $(event.currentTarget).closest('.o_chat_header').data('session-id');
var session = _.findWhere(hiddenSessions, {id: sessionID});
if (session) {
session.window.on_click_close(event);
self.displayState.windowsDropdownIsOpen = true; // keep the dropdown open
}
});
}
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {Object} channel
* @param {integer|string} channel.id
* @param {Object} query
*/
_onAnyoneListening: function (channel, query) {
_.each(this.chatSessions, function (session) {
if (channel.id === session.id && session.window.thread.is_at_bottom() && !session.window.is_hidden) {
query.is_displayed = true;
}
});
},
/**
* @private
* @param {Object} channel
* @param {integer} channel.id
*/
_onDetachChannel: function (channel) {
var chatSession = _.findWhere(this.chatSessions, {id: channel.id});
if (!chatSession || chatSession.window.folded) {
this.call('chat_manager', 'detachChannel', channel.id);
} else if (chatSession.window.is_hidden) {
this._makeSessionVisible(chatSession);
} else {
chatSession.window.focus_input();
}
},
/**
* @private
* @param {boolean} open
*/
_onDiscussOpen: function (open) {
this.displayState.chatWindowsHidden = open;
if (open) {
$('body').addClass('o_no_chat_window');
} else {
$('body').removeClass('o_no_chat_window');
this._repositionWindows();
}
},
/**
* @private
* @param {Object} message
*/
_onNewMessage: function (message) {
this._updateSessions(message, true);
},
/**
* @private
* @param {Object} channel
* @param {integer} channel.unread_counter
*/
_onUpdateChannelUnreadCounter: function (channel) {
var self = this;
this.displayState.hiddenUnreadCounter = 0;
_.each(self.chatSessions, function (session) {
if (channel.id === session.id) {
session.window.update_unread(channel.unread_counter);
if (channel.unread_counter === 0) {
session.keep_unread = false;
}
}
if (session.window.is_hidden) {
self.displayState.hiddenUnreadCounter += session.window.unread_msgs;
}
});
if (self.displayState.$hidden_windows_dropdown) {
self.displayState.$hidden_windows_dropdown.html(self.renderHiddenSessionsDropdown().html());
self._repositionHiddenSessionsDropdown();
}
},
/**
* @private
* @param {Object} channel
* @param {string|integer} channel.id
* @param {string} channel.status e.g. 'offline'
*/
_onUpdateDmPresence: function (channel) {
_.each(this.chatSessions, function (session) {
if (channel.id === session.id) {
session.window.update_status(channel.status);
}
});
},
/**
* @private
* @param {Object} message
*/
_onUpdateMessage: function (message) {
this._updateSessions(message, false);
},
/**
* @private
* @param {string|integer} channelID
*/
_onUnsubscribeFromChannel: function (channelID) {
var self = this;
_.each(self.chatSessions, function (session) {
if (channelID === session.id) {
self._closeChat(session);
}
});
},
});
return ChatWindowManager;
});
+24 -19
View File
@@ -7,8 +7,6 @@ var session = require('web.session');
var SystrayMenu = require('web.SystrayMenu');
var Widget = require('web.Widget');
var chat_manager = require('mail.chat_manager');
var QWeb = core.qweb;
/**
@@ -35,16 +33,20 @@ var MessagingMenu = Widget.extend({
this.$filter_buttons = this.$('.o_filter_button');
this.$channels_preview = this.$('.o_mail_navbar_dropdown_channels');
this.filter = false;
chat_manager.bus.on("update_needaction", this, this.update_counter);
chat_manager.bus.on("update_channel_unread_counter", this, this.update_counter);
chat_manager.is_ready.then(this.update_counter.bind(this));
var chatBus = this.call('chat_manager', 'getChatBus');
chatBus.on("update_needaction", this, this.update_counter);
chatBus.on("update_channel_unread_counter", this, this.update_counter);
var chatReady = this.call('chat_manager', 'isReady');
chatReady.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_needaction_counter() + chat_manager.get_unread_conversation_counter();
var needactionCounter = this.call('chat_manager', 'getNeedactionCounter');
var unreadConversationCounter = this.call('chat_manager', 'getUnreadConversationCounter');
var counter = needactionCounter + unreadConversationCounter;
this.$('.o_notification_counter').text(counter);
this.$el.toggleClass('o_no_notification', !counter);
if (this.is_open()) {
@@ -56,9 +58,10 @@ var MessagingMenu = Widget.extend({
// Display spinner while waiting for channels preview
this.$channels_preview.html(QWeb.render('Spinner'));
chat_manager.is_ready.then(function () {
var channels = _.filter(chat_manager.get_channels(), function (channel) {
var chatReady = this.call('chat_manager', 'isReady');
chatReady.then(function () {
var allChannels = self.call('chat_manager', 'getChannels');
var channels = _.filter(allChannels, function (channel) {
if (self.filter === 'chat') {
return channel.is_chat;
} else if (self.filter === 'channels') {
@@ -67,7 +70,7 @@ var MessagingMenu = Widget.extend({
return channel.type !== 'static';
}
});
chat_manager.get_messages({channel_id: 'channel_inbox'}).then(function(result) {
self.call('chat_manager', 'getMessages', {channelID: 'channel_inbox'}).then(function (result) {
var res = [];
_.each(result, function (message) {
message.unread_counter = 1;
@@ -82,7 +85,7 @@ var MessagingMenu = Widget.extend({
if (self.filter === 'channel_inbox' || !self.filter) {
channels = _.union(channels, res);
}
chat_manager.get_channels_preview(channels).then(self._render_channels_preview.bind(self));
self.call('chat_manager', 'getChannelsPreview', channels).then(self._render_channels_preview.bind(self));
});
});
},
@@ -105,7 +108,7 @@ var MessagingMenu = Widget.extend({
this.update_channels_preview();
},
on_click_new_message: function () {
chat_manager.bus.trigger('open_chat');
this.call('chat_window_manager', 'openChat');
},
// Handlers
@@ -133,13 +136,13 @@ var MessagingMenu = Widget.extend({
this.do_action('mail.mail_channel_action_client_chat', {clear_breadcrumbs: true})
.then(function () {
self.trigger_up('hide_home_menu'); // we cannot 'go back to previous page' otherwise
core.bus.trigger('change_menu_section', chat_manager.get_discuss_menu_id());
core.bus.trigger('change_menu_section', self.call('chat_manager', 'getDiscussMenuID').bind(self));
});
}
} else {
var channel = chat_manager.get_channel(channelID);
var channel = this.call('chat_manager', 'getChannel', channelID);
if (channel) {
chat_manager.open_channel(channel);
self.call('chat_manager', 'openChannel', channel);
}
}
},
@@ -156,8 +159,10 @@ var ActivityMenu = Widget.extend({
},
start: function () {
this.$activities_preview = this.$('.o_mail_navbar_dropdown_channels');
chat_manager.bus.on("activity_updated", this, this._updateCounter);
chat_manager.is_ready.then(this._updateCounter.bind(this));
var chatBus = this.call('chat_manager', 'getChatBus');
chatBus.on("activity_updated", this, this._updateCounter);
var chatReady = this.call('chat_manager', 'isReady');
chatReady.then(this._updateCounter.bind(this));
this._updateActivityPreview();
return this._super();
},
@@ -168,7 +173,7 @@ var ActivityMenu = Widget.extend({
* Make RPC and get current user's activity details
* @private
*/
_getActivityData: function(){
_getActivityData: function () {
var self = this;
return self._rpc({
@@ -179,7 +184,7 @@ var ActivityMenu = Widget.extend({
},
}).then(function (data) {
self.activities = data;
self.activityCounter = _.reduce(data, function(total_count, p_data){ return total_count + p_data.total_count; }, 0);
self.activityCounter = _.reduce(data, function (total_count, p_data) { return total_count + p_data.total_count; }, 0);
self.$('.o_notification_counter').text(self.activityCounter);
self.$el.toggleClass('o_no_notification', !self.activityCounter);
});
+8 -8
View File
@@ -142,7 +142,7 @@ var Thread = Widget.extend({
this.attachments = _.uniq(_.flatten(_.map(messages, 'attachment_ids')));
_.each(msgs, function(msg) {
_.each(msgs, function (msg) {
var $msg = self.$('.o_thread_message[data-message-id="'+ msg.id +'"]');
$msg.find('.o_mail_timestamp').data('date', msg.date);
@@ -150,7 +150,7 @@ var Thread = Widget.extend({
});
if (!this.update_timestamps_interval) {
this.update_timestamps_interval = setInterval(function() {
this.update_timestamps_interval = setInterval(function () {
self.update_timestamps();
}, 1000*60);
}
@@ -172,17 +172,17 @@ var Thread = Widget.extend({
// nodeType 1: element_node
// nodeType 3: text_node
var $children = $element.contents()
.filter(function() {
.filter(function () {
return this.nodeType === 1 || this.nodeType === 3 && this.nodeValue.trim();
});
_.each($children, function(child) {
_.each($children, function (child) {
var $child = $(child);
// Hide Text nodes if "stopSpelling"
if (child.nodeType === 3 && $child.prevAll("[id*='stopSpelling']").length > 0) {
// Convert Text nodes to Element nodes
var $child = $('<span>', {
$child = $('<span>', {
text: child.textContent,
"data-o-mail-quote": "1",
});
@@ -203,7 +203,7 @@ var Thread = Widget.extend({
}
});
_.each(groups, function(group) {
_.each(groups, function (group) {
// Insert link just before the first node
var $read_more = $('<a>', {
class: "o_mail_read_more",
@@ -213,7 +213,7 @@ var Thread = Widget.extend({
// Toggle All next nodes
var is_read_more = true;
$read_more.click(function(e) {
$read_more.click(function (e) {
e.preventDefault();
is_read_more = !is_read_more;
_.each(group, function ($child) {
@@ -226,7 +226,7 @@ var Thread = Widget.extend({
},
update_timestamps: function () {
var isAtBottom = this.is_at_bottom();
this.$('.o_mail_timestamp').each(function() {
this.$('.o_mail_timestamp').each(function () {
var date = $(this).data('date');
$(this).html(time_from_now(date));
});
+14 -13
View File
@@ -1,7 +1,6 @@
odoo.define('mail.ThreadField', function (require) {
"use strict";
var chat_mixin = require('mail.chat_mixin');
var ChatThread = require('mail.ChatThread');
var AbstractField = require('web.AbstractField');
@@ -15,14 +14,15 @@ var _t = core._t;
// -----------------------------------------------------------------------------
// 'mail_thread' widget: displays the thread of messages
// -----------------------------------------------------------------------------
var ThreadField = AbstractField.extend(chat_mixin, {
var ThreadField = AbstractField.extend({
// inherited
init: function () {
this._super.apply(this, arguments);
this.msgIDs = this.value.res_ids;
},
willStart: function () {
return this.alive(this._chatReady());
var chatReady = this.call('chat_manager', 'isReady');
return this.alive(chatReady);
},
start: function () {
var self = this;
@@ -39,7 +39,9 @@ var ThreadField = AbstractField.extend(chat_mixin, {
this.thread.on('load_more_messages', this, this._onLoadMoreMessages);
this.thread.on('redirect', this, this._onRedirect);
this.thread.on('redirect_to_channel', this, this._onRedirectToChannel);
this.thread.on('toggle_star_status', this, this._toggleStarStatus);
this.thread.on('toggle_star_status', this, function (messageID) {
self.call('chat_manager', 'toggleStarStatus', messageID);
});
var def1 = this.thread.appendTo(this.$el);
var def2 = this._super.apply(this, arguments);
@@ -47,10 +49,9 @@ var ThreadField = AbstractField.extend(chat_mixin, {
return this.alive($.when(def1, def2)).then(function () {
// unwrap the thread to remove an unnecessary level on div
self.setElement(self.thread.$el);
var bus = self._getBus();
bus.on('new_message', self, self._onNewMessage);
bus.on('update_message', self, self._onUpdateMessage);
var chatBus = self.call('chat_manager', 'getChatBus');
chatBus.on('new_message', self, self._onNewMessage);
chatBus.on('update_message', self, self._onUpdateMessage);
});
},
_render: function () {
@@ -60,7 +61,7 @@ var ThreadField = AbstractField.extend(chat_mixin, {
return true;
},
destroy: function () {
this._removeChatterMessages(this.model);
this.call('chat_manager', 'removeChatterMessages', this.model);
this._super.apply(this, arguments);
},
_reset: function (record) {
@@ -82,7 +83,7 @@ var ThreadField = AbstractField.extend(chat_mixin, {
postMessage: function (message) {
var self = this;
var options = {model: this.model, res_id: this.res_id};
return this._postMessage(message, options)
return this.call('chat_manager', 'postMessage', message, options)
.then(function () {
if (message.partner_ids.length) {
self.trigger_up('reload_mail_fields', {followers: true});
@@ -123,7 +124,7 @@ var ThreadField = AbstractField.extend(chat_mixin, {
var self = this;
options = options || {};
options.ids = ids;
var fetch_def = this.dp.add(this._getMessages(options));
var fetch_def = this.dp.add(this.call('chat_manager', 'getMessages', options));
return fetch_def.then(function (raw_messages) {
var isCreateMode = false;
if (!self.res_id) {
@@ -147,7 +148,7 @@ var ThreadField = AbstractField.extend(chat_mixin, {
* @returns {Deferred}
*/
_onLoadMoreMessages: function () {
this._fetchAndRenderThread(this.msgIDs, {force_fetch: true});
this._fetchAndRenderThread(this.msgIDs, {forceFetch: true});
},
_onNewMessage: function (message) {
if (message.model === this.model && message.res_id === this.res_id) {
@@ -161,7 +162,7 @@ var ThreadField = AbstractField.extend(chat_mixin, {
},
_onRedirectToChannel: function (channelID) {
var self = this;
this._joinChannel(channelID).then(function () {
this.call('chat_manager', 'joinChannel', channelID).then(function () {
// Execute Discuss client action with 'channel' as default channel
self.do_action('mail.mail_channel_action_client_chat', {active_id: channelID});
});
-43
View File
@@ -1,48 +1,6 @@
odoo.define('mail.utils', function (require) {
"use strict";
var bus = require('bus.bus').bus;
function send_notification(widget, title, content) {
if (window.Notification && Notification.permission === "granted") {
if (bus.is_master) {
_send_native_notification(title, content);
}
} else {
widget.do_notify(title, content);
if (bus.is_master) {
_beep(widget);
}
}
}
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;
return function (widget) {
if (!audio) {
audio = new Audio();
var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3";
var session = widget.getSession();
audio.src = session.url("/mail/static/src/audio/ting" + ext);
}
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);
@@ -141,7 +99,6 @@ function unaccent (str) {
}
return {
send_notification: send_notification,
parse_and_transform: parse_and_transform,
add_link: add_link,
linkify: linkify,
-400
View File
@@ -1,400 +0,0 @@
odoo.define('mail.window_manager', function (require) {
"use strict";
var chat_manager = require('mail.chat_manager');
var ExtendedChatWindow = require('mail.ExtendedChatWindow');
var config = require('web.config');
var core = require('web.core');
var utils = require('web.utils');
var web_client = require('web.web_client');
var _t = core._t;
var QWeb = core.qweb;
// chat window management
//----------------------------------------------------------------
var CHAT_WINDOW_WIDTH = 325 + 5; // 5 pixels between windows
var chat_sessions = [];
var new_chat_session;
var display_state = {
chat_windows_hidden: false, // chat windows aren't displayed when the client action is open
hidden_sessions: [],
hidden_unread_counter: 0, // total number of unread msgs in hidden chat windows
nb_slots: 0,
space_left: 0,
windows_dropdown_is_open: false, // used to keep dropdown open when closing chat windows
};
function add_chat_session (chat_session) {
// adds chat_session such that it will be the left-most visible window
compute_available_slots(chat_sessions.length+1);
chat_sessions.splice(display_state.nb_slots-1, 0, chat_session);
}
// options.passively: if set to true, open the chat window without focusing the
// input and marking messages as read if it is not open yet, and do nothing
// otherwise
function open_chat (session, options) {
if (!session) {
open_chat_without_session();
return;
}
options = options || {};
var chat_session = _.findWhere(chat_sessions, {id: session.id});
if (!chat_session) {
var prefix = !session.is_chat ? "#" : "";
var window_options = {
autofocus: !options.passively,
input_less: session.mass_mailing,
status: session.status,
};
chat_session = {
id: session.id,
uuid: session.uuid,
name: session.name,
keep_unread: options.passively, // don't automatically mark unread messages as seen
window: new ExtendedChatWindow(web_client, session.id, prefix + session.name, session.is_folded, session.unread_counter, window_options),
};
chat_session.window.on("close_chat_session", null, function () {
close_chat(chat_session);
chat_manager.close_chat_session(chat_session.id);
});
chat_session.window.on("toggle_star_status", null, function (message_id) {
chat_manager.toggle_star_status(message_id);
});
chat_session.window.on("fold_channel", null, function (channel_id, folded) {
chat_manager.fold_channel(channel_id, folded);
});
chat_session.window.on("post_message", null, function (message, channel_id) {
chat_manager
.post_message(message, {channel_id: channel_id})
.then(function () {
chat_session.window.thread.scroll_to();
});
});
chat_session.window.on("redirect", null, function (res_model, res_id) {
chat_manager.redirect(res_model, res_id, open_chat);
});
chat_session.window.on("redirect_to_channel", null, function (channel_id) {
var session = _.findWhere(chat_sessions, {id: channel_id});
if (!session) {
chat_manager.join_channel(channel_id).then(function (channel) {
chat_manager.detach_channel(channel);
});
} else {
session.window.toggle_fold(false);
}
});
var remove_new_chat = false;
if (options.passively) {
chat_sessions.push(chat_session); // simply insert the window to the left
} else if (new_chat_session && new_chat_session.partner_id && new_chat_session.partner_id === session.direct_partner_id) {
// the window takes the place of the 'new_chat_session' window
chat_sessions[_.indexOf(chat_sessions, new_chat_session)] = chat_session;
remove_new_chat = true;
} else {
add_chat_session(chat_session); // add session such that window is visible
}
chat_session.window.appendTo($('body'))
.then(function () {
reposition_windows({remove_new_chat: remove_new_chat});
return chat_manager.get_messages({channel_id: chat_session.id});
}).then(function (messages) {
chat_session.window.render(messages);
chat_session.window.thread.scroll_to();
setTimeout(function () {
chat_session.window.thread.$el.on("scroll", null, _.debounce(function () {
if (!chat_session.keep_unread && chat_session.window.thread.is_at_bottom()) {
chat_manager.mark_channel_as_seen(session);
}
}, 100));
}, 0); // setTimeout to prevent to execute handler on first scroll_to, which is asynchronous
if (options.passively) {
// mark first unread messages as seen when focusing the window, then on scroll to bottom as usual
chat_session.window.$('.o_mail_thread, .o_chat_composer').one('click', function () {
chat_manager.mark_channel_as_seen(session);
});
} else if (!display_state.chat_windows_hidden && !session.is_folded) {
chat_manager.mark_channel_as_seen(session);
}
});
} else if (!options.passively) {
if (chat_session.window.is_hidden) {
make_session_visible(chat_session);
} else if (session.is_folded !== chat_session.window.folded) {
chat_session.window.toggle_fold(session.is_folded);
}
}
}
function open_chat_without_session () {
if (!new_chat_session) {
new_chat_session = {
id: '_open',
window: new ExtendedChatWindow(web_client, undefined, _t('New message'), false, 0, {thread_less: true}),
};
new_chat_session.window.on("close_chat_session", null, close_new_chat);
new_chat_session.window.on('open_dm_session', null, function (partner_id) {
new_chat_session.partner_id = partner_id;
var dm = chat_manager.get_dm_from_partner_id(partner_id);
if (!dm) {
chat_manager.open_and_detach_dm(partner_id);
} else {
var dm_session = _.findWhere(chat_sessions, {id: dm.id});
if (!dm_session) {
chat_manager.detach_channel(dm);
} else {
close_chat(dm_session);
dm.is_folded = false;
open_chat(dm);
}
}
});
add_chat_session(new_chat_session);
new_chat_session.window.appendTo($('body')).then(reposition_windows);
} else {
if (new_chat_session.window.is_hidden) {
make_session_visible(new_chat_session);
} else if (new_chat_session.window.folded) {
new_chat_session.window.toggle_fold(false);
}
}
}
function close_chat (chat_session, options) {
if (options && options.keep_open_if_unread && chat_session.keep_unread) {
return;
}
chat_sessions = _.without(chat_sessions, chat_session);
chat_session.window.destroy();
reposition_windows();
}
function close_new_chat () {
chat_sessions = _.without(chat_sessions, new_chat_session);
reposition_windows({remove_new_chat: true});
}
function destroy_new_chat () {
new_chat_session.window.destroy();
new_chat_session = undefined;
}
function toggle_fold_chat (channel) {
var session = _.find(chat_sessions, {id: channel.id});
if (session) {
session.window.toggle_fold(channel.is_folded);
}
}
function compute_available_slots (nb_windows) {
if (config.device.isMobile) {
display_state.nb_slots = 1; // one chat window full screen in mobile
return;
}
var width = window.innerWidth;
var nb_slots = Math.floor(width/CHAT_WINDOW_WIDTH);
var space_left = width - (Math.min(nb_slots, nb_windows)*CHAT_WINDOW_WIDTH);
if (nb_slots < nb_windows && space_left < 50) {
nb_slots--; // leave at least 50px for the hidden windows dropdown button
space_left += CHAT_WINDOW_WIDTH;
}
display_state.nb_slots = nb_slots;
display_state.space_left = space_left;
}
var reposition_windows = function (options) {
if (options && options.remove_new_chat) {
destroy_new_chat();
}
if (display_state.chat_windows_hidden) {
return;
}
compute_available_slots(chat_sessions.length);
var hidden_sessions = [];
var hidden_unread_counter = 0;
var nb_slots = display_state.nb_slots;
_.each(chat_sessions, function (session, index) {
if (index < nb_slots) {
session.window.$el.css({right: CHAT_WINDOW_WIDTH*index, bottom: 0});
session.window.do_show();
} else {
hidden_sessions.push(session);
hidden_unread_counter += session.window.unread_msgs;
session.window.do_hide();
}
});
display_state.hidden_sessions = hidden_sessions;
display_state.hidden_unread_counter = hidden_unread_counter;
if (display_state.$hidden_windows_dropdown) {
display_state.$hidden_windows_dropdown.remove();
}
if (hidden_sessions.length) {
display_state.$hidden_windows_dropdown = render_hidden_sessions_dropdown();
var $hidden_windows_dropdown = display_state.$hidden_windows_dropdown;
$hidden_windows_dropdown.css({right: CHAT_WINDOW_WIDTH * nb_slots, bottom: 0})
.appendTo($('body'));
reposition_hidden_sessions_dropdown();
display_state.windows_dropdown_is_open = false;
$hidden_windows_dropdown.on('click', '.o_chat_header', function (event) {
var session_id = $(event.currentTarget).data('session-id');
var session = _.findWhere(hidden_sessions, {id: session_id});
if (session) {
make_session_visible(session);
}
});
$hidden_windows_dropdown.on('click', '.o_chat_window_close', function (event) {
var session_id = $(event.currentTarget).closest('.o_chat_header').data('session-id');
var session = _.findWhere(hidden_sessions, {id: session_id});
if (session) {
session.window.on_click_close(event);
display_state.windows_dropdown_is_open = true; // keep the dropdown open
}
});
}
};
function make_session_visible (session) {
utils.swap(chat_sessions, session, chat_sessions[display_state.nb_slots-1]);
reposition_windows();
session.window.toggle_fold(false);
}
function render_hidden_sessions_dropdown () {
var $dropdown = $(QWeb.render("mail.ChatWindowsDropdown", {
sessions: display_state.hidden_sessions,
open: display_state.windows_dropdown_is_open,
unread_counter: display_state.hidden_unread_counter,
widget: {isMobile: config.device.isMobile},
}));
return $dropdown;
}
function reposition_hidden_sessions_dropdown () {
// Unfold dropdown to the left if there is enough place
var $dropdown_ul = display_state.$hidden_windows_dropdown.children('ul');
if (display_state.space_left > $dropdown_ul.width() + 10) {
$dropdown_ul.addClass('dropdown-menu-right');
}
}
function update_sessions (message, scrollBottom) {
_.each(chat_sessions, function (session) {
if (_.contains(message.channel_ids, session.id)) {
var message_visible = !display_state.chat_windows_hidden && !session.window.folded &&
!session.window.is_hidden && session.window.thread.is_at_bottom();
if (message_visible && !session.keep_unread) {
chat_manager.mark_channel_as_seen(chat_manager.get_channel(session.id));
}
chat_manager.get_messages({channel_id: session.id}).then(function (messages) {
session.window.render(messages);
if (scrollBottom && message_visible) {
session.window.thread.scroll_to();
}
});
}
});
}
core.bus.on('web_client_ready', null, function () {
chat_manager.bus.on('open_chat', null, open_chat);
chat_manager.bus.on('close_chat', null, function (channel, options) {
var session = _.find(chat_sessions, {id: channel.id});
if (session) {
close_chat(session, options);
}
});
chat_manager.bus.on('channel_toggle_fold', null, toggle_fold_chat);
chat_manager.bus.on('new_message', null, function (message) {
update_sessions(message, true);
});
chat_manager.bus.on('update_message', null, function (message) {
update_sessions(message, false);
});
chat_manager.bus.on('anyone_listening', null, function (channel, query) {
_.each(chat_sessions, function (session) {
if (channel.id === session.id && session.window.thread.is_at_bottom() && !session.window.is_hidden) {
query.is_displayed = true;
}
});
});
chat_manager.bus.on('unsubscribe_from_channel', null, function (channel_id) {
_.each(chat_sessions, function (session) {
if (channel_id === session.id) {
close_chat(session);
}
});
});
chat_manager.bus.on('update_channel_unread_counter', null, function (channel) {
display_state.hidden_unread_counter = 0;
_.each(chat_sessions, function (session) {
if (channel.id === session.id) {
session.window.update_unread(channel.unread_counter);
if (channel.unread_counter === 0) {
session.keep_unread = false;
}
}
if (session.window.is_hidden) {
display_state.hidden_unread_counter += session.window.unread_msgs;
}
});
if (display_state.$hidden_windows_dropdown) {
display_state.$hidden_windows_dropdown.html(render_hidden_sessions_dropdown().html());
reposition_hidden_sessions_dropdown();
}
});
chat_manager.bus.on('update_dm_presence', null, function (channel) {
_.each(chat_sessions, function (session) {
if (channel.id === session.id) {
session.window.update_status(channel.status);
}
});
});
chat_manager.is_ready.then(function() {
_.each(chat_manager.get_channels(), function (channel) {
if (channel.is_detached) {
open_chat(channel);
}
});
});
chat_manager.bus.on('detach_channel', null, function (channel) {
var chat_session = _.findWhere(chat_sessions, {id: channel.id});
if (!chat_session || chat_session.window.folded) {
chat_manager.detach_channel(channel);
} else if (chat_session.window.is_hidden) {
make_session_visible(chat_session);
} else {
chat_session.window.focus_input();
}
});
chat_manager.bus.on('client_action_open', null, function (open) {
display_state.chat_windows_hidden = open;
if (open) {
$('body').addClass('o_no_chat_window');
} else {
$('body').removeClass('o_no_chat_window');
reposition_windows();
}
});
core.bus.on('resize', null, _.debounce(reposition_windows, 100));
});
});
@@ -356,7 +356,7 @@
}
// ------------------------------------------------------------------
// Channel preview: shared between client action (mobile) and systray
// Channel preview: shared between discuss (mobile) and systray
// ------------------------------------------------------------------
.o_mail_channel_preview {
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<!-- Client Action -->
<t t-name="mail.client_action">
<!-- Discuss -->
<t t-name="mail.discuss">
<div class="o_mail_chat">
<div class="o_mail_chat_sidebar"/>
<div class="o_mail_chat_content">
@@ -147,7 +147,7 @@
<!-- Mobile templates -->
<t t-name="mail.client_action_mobile">
<t t-name="mail.discuss_mobile">
<div class="o_mail_chat">
<div class="o_mail_chat_mobile_control_panel"/>
<div class="o_mail_chat_mobile_inbox_buttons">
@@ -193,7 +193,7 @@
</div>
</t>
<!-- The ChannelPreview template is used by the client action in mobile, and by the systray menu -->
<!-- The ChannelPreview template is used by the discuss in mobile, and by the systray menu -->
<t t-name="mail.chat.ChannelPreview">
<div t-attf-class="o_mail_channel_preview #{channel.unread_counter ? 'o_channel_unread' : ''}"
t-att-data-channel_id="channel.id" t-att-data-res_id="channel.res_id" t-att-data-res_model="channel.model">
@@ -1,10 +1,15 @@
odoo.define('mail.chat_window_test', function (require) {
"use strict";
var ChatManager = require('mail.ChatManager');
var ChatWindow = require('mail.ExtendedChatWindow');
var mailTestUtils = require('mail.testUtils');
var framework = require('web.framework');
var testUtils = require('web.test_utils');
var Widget = require('web.Widget');
var ChatWindow = require('mail.ExtendedChatWindow');
var createBusService = mailTestUtils.createBusService;
QUnit.module('mail', {}, function () {
@@ -88,6 +93,8 @@ QUnit.module('mail', {}, function () {
QUnit.test('emoji popover should open correctly in chat windows', function (assert) {
assert.expect(1);
var BusService = createBusService();
function createParent(params) {
var widget = new Widget();
@@ -100,6 +107,7 @@ QUnit.module('mail', {}, function () {
}
var parent = createParent({
data: {},
services: [ChatManager, BusService],
});
var $dom = $('#qunit-fixture');
+99 -169
View File
@@ -2,6 +2,8 @@ odoo.define('mail.chatter_tests', function (require) {
"use strict";
var Composers = require('mail.composer');
var ChatManager = require('mail.ChatManager');
var mailTestUtils = require('mail.testUtils');
var Bus = require('web.Bus');
var concurrency = require('web.concurrency');
@@ -11,6 +13,7 @@ var testUtils = require('web.test_utils');
var BasicComposer = Composers.BasicComposer;
var createBusService = mailTestUtils.createBusService;
var createAsyncView = testUtils.createAsyncView;
var createView = testUtils.createView;
@@ -18,6 +21,7 @@ QUnit.module('mail', {}, function () {
QUnit.module('Chatter', {
beforeEach: function () {
this.BusService = createBusService();
this.data = {
partner: {
fields: {
@@ -104,9 +108,8 @@ QUnit.test('basic rendering', function (assert) {
'<field name="activity_ids" widget="mail_activity"/>' +
'</div>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
if ('/web/dataset/call_kw/mail.followers/read' === route) {
if (route === '/web/dataset/call_kw/mail.followers/read') {
unwanted_read_count++;
}
if (route === '/mail/read_followers') {
@@ -118,28 +121,8 @@ QUnit.test('basic rendering', function (assert) {
}
return this._super(route, args);
},
intercepts: {
get_messages: function (event) {
// msgRpc++;
event.stopPropagation();
event.data.callback($.when([{
attachment_ids: [],
body: "",
date: moment("2016-12-20 09:35:40"),
id: 34,
res_id: 3,
author_id: ["3", "Fu Ck Mil Grom"],
}]));
},
get_bus: function (event) {
event.stopPropagation();
event.data.callback(new Bus());
},
get_session: function (event) {
event.stopPropagation();
event.data.callback({uid: 1});
},
},
services: [ChatManager, this.BusService],
res_id: 2,
});
assert.ok(form.$('.o_mail_activity').length, "there should be an activity widget");
@@ -182,16 +165,7 @@ QUnit.test('chatter in create mode', function (assert) {
}
return this._super(route, args);
},
intercepts: {
get_messages: function (event) {
event.stopPropagation();
event.data.callback($.when([]));
},
get_bus: function (event) {
event.stopPropagation();
event.data.callback(new Bus());
},
},
services: [ChatManager, this.BusService],
});
assert.strictEqual(form.$('.o_chatter').length, 1,
@@ -258,16 +232,7 @@ QUnit.test('chatter rendering inside the sheet', function (assert) {
}
return this._super(route, args);
},
intercepts: {
get_messages: function (event) {
event.stopPropagation();
event.data.callback($.when([]));
},
get_bus: function (event) {
event.stopPropagation();
event.data.callback(new Bus());
},
},
services: [ChatManager, this.BusService],
});
assert.strictEqual(form.$('.o_chatter').length, 1,
@@ -405,6 +370,9 @@ QUnit.test('chatter: post, receive and star messages', function (assert) {
var done = assert.async();
assert.expect(28);
var bus = new Bus();
var BusService = createBusService(bus);
// Remove the mention throttle to speed up the test
var mentionThrottle = BasicComposer.prototype.MENTION_THROTTLE;
BasicComposer.prototype.MENTION_THROTTLE = 1;
@@ -414,7 +382,7 @@ QUnit.test('chatter: post, receive and star messages', function (assert) {
attachment_ids: [],
author_id: ["1", "John Doe"],
body: "A message",
date: moment("2016-12-20 09:35:40"),
date: "2016-12-20 09:35:40",
displayed_author: "John Doe",
id: 1,
is_note: false,
@@ -423,7 +391,6 @@ QUnit.test('chatter: post, receive and star messages', function (assert) {
model: 'partner',
res_id: 2,
}];
var bus = new Bus();
var getSuggestionsDef = $.Deferred();
var form = createView({
View: FormView,
@@ -439,6 +406,12 @@ QUnit.test('chatter: post, receive and star messages', function (assert) {
'</form>',
res_id: 2,
mockRPC: function (route, args) {
if (args.method === 'message_format') {
var requested_msgs = _.filter(messages, function (msg) {
return _.contains(args.args[0], msg.id);
});
return $.when(requested_msgs);
}
if (args.method === 'message_get_suggested_recipients') {
return $.when({2: []});
}
@@ -446,52 +419,43 @@ QUnit.test('chatter: post, receive and star messages', function (assert) {
getSuggestionsDef.resolve();
return $.when([{email: "test@odoo.com", id: 1, name: "Test User"}]);
}
return this._super(route, args);
},
session: {},
intercepts: {
get_messages: function (event) {
event.stopPropagation();
var requested_msgs = _.filter(messages, function (msg) {
return _.contains(event.data.options.ids, msg.id);
});
event.data.callback($.when(requested_msgs));
},
post_message: function (event) {
event.stopPropagation();
if (args.method === 'message_post') {
var msg_id = messages[messages.length-1].id + 1;
messages.push({
attachment_ids: [],
attachment_ids: args.kwargs.attachment_ids,
author_id: ["42", "Me"],
body: event.data.message.content,
date: moment(), // now
body: args.kwargs.body,
date: "2016-12-20 10:35:40",
displayed_author: "Me",
id: msg_id,
is_note: event.data.message.subtype === 'mail.mt_note',
is_discussion: event.data.message.subtype === 'mail.mt_comment',
is_note: args.kwargs.subtype === 'mail.mt_note',
is_discussion: args.kwargs.subtype === 'mail.mt_comment',
is_starred: false,
model: 'partner',
res_id: 2,
});
bus.trigger('new_message', {
id: msg_id,
model: event.data.options.model,
res_id: event.data.options.res_id,
});
},
get_bus: function (event) {
event.stopPropagation();
event.data.callback(bus);
},
toggle_star_status: function (event) {
event.stopPropagation();
assert.strictEqual(event.data.message_id, 2,
return $.when(msg_id);
}
if (args.method === 'toggle_message_starred') {
assert.ok(_.contains(args.args[0], 2),
"toggle_star_status should have been triggered for message 2 (twice)");
var msg = _.findWhere(messages, {id: event.data.message_id});
var msg = _.findWhere(messages, {id: args.args[0][0]});
msg.is_starred = !msg.is_starred;
bus.trigger('update_message', msg);
},
// simulate notification received by chat_manager from longpoll
var data = {
info: false,
message_ids: [msg.id],
starred: msg.is_starred,
type: 'toggle_star',
};
var notification = [[false, 'res.partner'], data];
bus.trigger('notification', [notification]);
return $.when();
}
return this._super(route, args);
},
session: {},
services: [ChatManager, BusService],
});
assert.ok(form.$('.o_chatter_topbar .o_chatter_button_log_note').length,
@@ -578,7 +542,6 @@ QUnit.test('chatter: post a message and switch in edit mode', function (assert)
assert.expect(5);
var messages = [];
var bus = new Bus();
var form = createView({
View: FormView,
model: 'partner',
@@ -594,45 +557,35 @@ QUnit.test('chatter: post a message and switch in edit mode', function (assert)
res_id: 2,
session: {},
mockRPC: function (route, args) {
if (route === "/web/dataset/call_kw/partner/message_get_suggested_recipients") {
if (args.method === 'message_format') {
var requested_msgs = _.filter(messages, function (msg) {
return _.contains(args.args[0], msg.id);
});
return $.when(requested_msgs);
}
if (args.method === 'message_get_suggested_recipients') {
return $.when({2: []});
}
return this._super(route, args);
},
intercepts: {
get_messages: function (event) {
event.stopPropagation();
var requested_msgs = _.filter(messages, function (msg) {
return _.contains(event.data.options.ids, msg.id);
});
event.data.callback($.when(requested_msgs));
},
post_message: function (event) {
event.stopPropagation();
if (args.method === 'message_post') {
messages.push({
attachment_ids: [],
attachment_ids: args.kwargs.attachment_ids,
author_id: ["42", "Me"],
body: event.data.message.content,
date: moment(), // now
body: args.kwargs.body,
date: "2016-12-20 10:35:40",
displayed_author: "Me",
id: 42,
is_note: event.data.message.subtype === 'mail.mt_note',
is_discussion: event.data.message.subtype === 'mail.mt_comment',
is_note: args.kwargs.subtype === 'mail.mt_note',
is_discussion: args.kwargs.subtype === 'mail.mt_comment',
is_starred: false,
model: 'partner',
res_id: 2,
});
bus.trigger('new_message', {
id: 42,
model: event.data.options.model,
res_id: event.data.options.res_id,
});
},
get_bus: function (event) {
event.stopPropagation();
event.data.callback(bus);
},
return $.when(42);
}
return this._super(route, args);
},
services: [ChatManager, this.BusService],
});
assert.strictEqual(form.$('.o_thread_message').length, 0, "thread should not contain messages");
@@ -685,7 +638,7 @@ QUnit.test('chatter: Attachment viewer', function (assert) {
}],
author_id: ["1", "John Doe"],
body: "Attachement viewer test",
date: moment("2016-12-20 09:35:40"),
date: "2016-12-20 09:35:40",
displayed_author: "John Doe",
id: 1,
is_note: false,
@@ -694,6 +647,7 @@ QUnit.test('chatter: Attachment viewer', function (assert) {
model: 'partner',
res_id: 2,
}];
var form = createView({
View: FormView,
model: 'partner',
@@ -707,27 +661,21 @@ QUnit.test('chatter: Attachment viewer', function (assert) {
'</div>' +
'</form>',
res_id: 2,
mockRPC: function (route) {
if(_.str.contains(route, '/mail/attachment/preview/') ||
mockRPC: function (route, args) {
if (args.method === 'message_format') {
var requested_msgs = _.filter(messages, function (msg) {
return _.contains(args.args[0], msg.id);
});
return $.when(requested_msgs);
}
if (_.str.contains(route, '/mail/attachment/preview/') ||
_.str.contains(route, '/web/static/lib/pdfjs/web/viewer.html')){
var canvas = document.createElement('canvas');
return $.when(canvas.toDataURL());
}
return this._super.apply(this, arguments);
},
intercepts: {
get_messages: function (event) {
event.stopPropagation();
var requested_msgs = _.filter(messages, function (msg) {
return _.contains(event.data.options.ids, msg.id);
});
event.data.callback($.when(requested_msgs));
},
get_bus: function (event) {
event.stopPropagation();
event.data.callback(new Bus());
},
},
services: [ChatManager, this.BusService],
});
assert.strictEqual(form.$('.o_thread_message .o_attachment').length, 4,
"there should be three attachment on message");
@@ -783,6 +731,9 @@ QUnit.test('form activity widget: schedule next activity', function (assert) {
'</form>',
res_id: 2,
mockRPC: function (route, args) {
if (args.method === 'message_format') {
return $.when([]);
}
if (route === '/web/dataset/call_kw/mail.activity/action_feedback') {
assert.ok(_.isEqual(args.args[0], [1]), "should call 'action_feedback' for id 1");
assert.strictEqual(args.kwargs.feedback, 'everything is ok',
@@ -795,15 +746,8 @@ QUnit.test('form activity widget: schedule next activity', function (assert) {
}
return this._super.apply(this, arguments);
},
services: [ChatManager, this.BusService],
intercepts: {
get_messages: function (event) {
event.stopPropagation();
event.data.callback($.when([]));
},
get_bus: function (event) {
event.stopPropagation();
event.data.callback(new Bus());
},
do_action: function (event) {
assert.deepEqual(event.data.action, {
context: {
@@ -856,11 +800,8 @@ QUnit.test('form activity widget: schedule activity does not discard changes', f
}
return this._super.apply(this, arguments);
},
services: [ChatManager, this.BusService],
intercepts: {
get_bus: function (event) {
event.stopPropagation();
event.data.callback(new Bus());
},
do_action: function (event) {
event.data.options.on_close();
},
@@ -922,6 +863,12 @@ QUnit.test('form activity widget: mark as done and remove', function (assert) {
'</form>',
res_id: 2,
mockRPC: function (route, args) {
if (args.method === 'message_format') {
var requested_msgs = _.filter(messages, function (msg) {
return _.contains(args.args[0], msg.id);
});
return $.when(requested_msgs);
}
if (route === '/web/dataset/call_kw/mail.activity/unlink') {
assert.ok(_.isEqual(args.args[0], [1]), "should call 'unlink' for id 1");
} else if (route === '/web/dataset/call_kw/mail.activity/action_feedback') {
@@ -934,7 +881,7 @@ QUnit.test('form activity widget: mark as done and remove', function (assert) {
attachment_ids: [],
author_id: ["1", "John Doe"],
body: "The activity has been done",
date: moment("2016-12-20 09:35:40"),
date: "2016-12-20 09:35:40",
displayed_author: "John Doe",
id: 1,
is_note: true,
@@ -956,16 +903,7 @@ QUnit.test('form activity widget: mark as done and remove', function (assert) {
}
return this._super.apply(this, arguments);
},
intercepts: {
get_messages: function (event) {
event.stopPropagation();
event.data.callback($.when(messages));
},
get_bus: function (event) {
event.stopPropagation();
event.data.callback(new Bus());
}
},
services: [ChatManager, this.BusService],
});
assert.strictEqual(form.$('.o_mail_activity .o_thread_message').length, 2,
@@ -1203,6 +1141,16 @@ QUnit.test('does not render and crash when destroyed before chat system is ready
'</form>',
res_id: 2,
mockRPC: function (route, args) {
if (args.method === 'message_format') {
return $.when([{
attachment_ids: [],
body: "",
date: "2016-12-20 09:35:40",
id: 34,
res_id: 3,
author_id: ["3", "Fu Ck Mil Grom"],
}]);
}
if (route === '/mail/read_followers') {
return $.when({
followers: [],
@@ -1211,26 +1159,8 @@ QUnit.test('does not render and crash when destroyed before chat system is ready
}
return this._super(route, args);
},
services: [ChatManager, this.BusService],
intercepts: {
chat_manager_ready: function (event) {
// we delay the return of the chat_manager ready event
event.data.callback(def);
},
get_messages: function (event) {
event.stopPropagation();
event.data.callback($.when([{
attachment_ids: [],
body: "",
date: moment("2016-12-20 09:35:40"),
id: 34,
res_id: 3,
author_id: ["3", "Fu Ck Mil Grom"],
}]));
},
get_bus: function (event) {
event.stopPropagation();
event.data.callback(new Bus());
},
get_session: function (event) {
event.stopPropagation();
event.data.callback({uid: 1});
@@ -1239,10 +1169,10 @@ QUnit.test('does not render and crash when destroyed before chat system is ready
});
form.destroy();
// here, the chat_manager system is ready, and the chatter can try to render
// here, the chatManager system is ready, and the chatter can try to render
// itself. We simply make sure here that no crashes occur (since the form
// view is destroyed, all rpcs will be dropped, and many other mechanisms
// relying on events will not work, such as _getBus)
// relying on events will not work, such as the chat bus)
def.resolve();
});
@@ -1,105 +0,0 @@
odoo.define('mail.client_action_mobile_tests', function (require) {
"use strict";
var session = require('web.session');
var testUtils = require('web.test_utils');
var Widget = require('web.Widget');
var ChatAction = require('mail.chat_client_action');
var chat_manager = require('mail.chat_manager');
QUnit.module('mail', {}, function () {
QUnit.module('Discuss client action in mobile', {
beforeEach: function () {
this.data = {
'mail.message': {
fields: {},
},
};
this.createChatAction = function (params) {
var Parent = Widget.extend({
do_push_state: function () {},
});
var parent = new Parent();
testUtils.addMockEnvironment(parent, {
data: this.data,
archs: {
'mail.message,false,search': '<search/>',
},
session: params.session || {},
intercepts: params.intercepts || {},
});
var chatAction = new ChatAction(parent, params);
chatAction.set_cp_bus(new Widget());
chatAction.appendTo($('#qunit-fixture'));
return chatAction;
};
},
});
QUnit.test('mobile basic rendering', function (assert) {
// This is a very basic first test for the client action. However, with
// the chat_manager, it is hard to override RPCs (for instance, the
// /mail/client_action route is always called when the test suite is
// launched), and we must wait for this RPC to be done before starting to
// test the interface. This should be refactored to facilitate the testing.
assert.expect(9);
var done = assert.async();
var rpc = session.rpc;
var chatAction = this.createChatAction({
id: 1,
context: {},
params: {},
intercepts: {
get_session: function (ev) {
ev.data.callback({});
},
},
session: {
rpc: function (route, args) {
if (args.method === 'message_fetch') {
return $.when([]);
}
return rpc.apply(this, arguments);
},
},
});
chat_manager.is_ready.then(function () {
// test basic rendering in mobile
assert.strictEqual(chatAction.$('.o_mail_chat_mobile_control_panel').length, 1,
"should have rendered a control panel");
assert.strictEqual(chatAction.$('.o_mail_chat_content .o_mail_no_content').length, 1,
"should display the no content message");
assert.strictEqual(chatAction.$('.o_mail_mobile_tabs').length, 1,
"should have rendered the tabs");
assert.ok(chatAction.$('.o_mail_mobile_tab[data-type=channel_inbox]').hasClass('active'),
"should be in inbox tab");
assert.strictEqual(chatAction.$('.o_mail_chat_mobile_inbox_buttons:visible').length, 1,
"inbox/starred buttons should be visible");
assert.ok(chatAction.$('.o_mail_chat_mobile_inbox_buttons .o_channel_inbox_item[data-type=channel_inbox]').hasClass('btn-primary'),
"should be in inbox");
// move to DMs tab
chatAction.$('.o_mail_mobile_tab[data-type=dm]').click();
assert.ok(chatAction.$('.o_mail_mobile_tab[data-type=dm]').hasClass('active'),
"should be in DMs tab");
assert.strictEqual(chatAction.$('.o_mail_chat_content .o_mail_no_content').length, 0,
"should display the no content message");
chatAction.$('.o_mail_chat_button_dm').click(); // click to add a channel
assert.strictEqual(chatAction.$('.o_mail_add_channel input:visible').length, 1,
"should display the input to add a channel");
chatAction.destroy();
done();
});
});
});
});
@@ -0,0 +1,78 @@
odoo.define('mail.discuss_mobile_tests', function (require) {
"use strict";
var ChatManager = require('mail.ChatManager');
var mailTestUtils = require('mail.testUtils');
var createBusService = mailTestUtils.createBusService;
var createDiscuss = mailTestUtils.createDiscuss;
QUnit.module('mail', {}, function () {
QUnit.module('Discuss client action in mobile', {
beforeEach: function () {
this.BusService = createBusService();
this.data = {
'mail.message': {
fields: {},
},
};
},
});
QUnit.test('mobile basic rendering', function (assert) {
// This is a very basic first test for the client action. However, with
// the chat_manager, it is hard to override RPCs (for instance, the
// /mail/client_action route is always called when the test suite is
// launched), and we must wait for this RPC to be done before starting to
// test the interface. This should be refactored to facilitate the testing.
assert.expect(9);
var done = assert.async();
var discuss = createDiscuss({
id: 1,
context: {},
params: {},
mockRPC: function (route, args) {
if (args.method === 'message_fetch') {
return $.when([]);
}
return this._super.apply(this, arguments);
},
services: [ChatManager, this.BusService],
});
var chatReady = discuss.call('chat_manager', 'isReady');
chatReady.then(function () {
// test basic rendering in mobile
assert.strictEqual(discuss.$('.o_mail_chat_mobile_control_panel').length, 1,
"should have rendered a control panel");
assert.strictEqual(discuss.$('.o_mail_chat_content .o_mail_no_content').length, 1,
"should display the no content message");
assert.strictEqual(discuss.$('.o_mail_mobile_tabs').length, 1,
"should have rendered the tabs");
assert.ok(discuss.$('.o_mail_mobile_tab[data-type=channel_inbox]').hasClass('active'),
"should be in inbox tab");
assert.strictEqual(discuss.$('.o_mail_chat_mobile_inbox_buttons:visible').length, 1,
"inbox/starred buttons should be visible");
assert.ok(discuss.$('.o_mail_chat_mobile_inbox_buttons .o_channel_inbox_item[data-type=channel_inbox]').hasClass('btn-primary'),
"should be in inbox");
// move to DMs tab
discuss.$('.o_mail_mobile_tab[data-type=dm]').click();
assert.ok(discuss.$('.o_mail_mobile_tab[data-type=dm]').hasClass('active'),
"should be in DMs tab");
assert.strictEqual(discuss.$('.o_mail_chat_content .o_mail_no_content').length, 0,
"should display the no content message");
discuss.$('.o_mail_chat_button_dm').click(); // click to add a channel
assert.strictEqual(discuss.$('.o_mail_add_channel input:visible').length, 1,
"should display the input to add a channel");
discuss.destroy();
done();
});
});
});
});
+74
View File
@@ -0,0 +1,74 @@
odoo.define('mail.discuss_test', function (require) {
"use strict";
var testUtils = require('web.test_utils');
var Widget = require('web.Widget');
var mailTestUtils = require('mail.testUtils');
var createDiscuss = mailTestUtils.createDiscuss;
QUnit.module('mail', {}, function () {
QUnit.module('Discuss client action', {
beforeEach: function () {
this.data = {
'mail.message': {
fields: {},
},
};
},
});
QUnit.skip('mobile basic rendering', function (assert) {
// Unfortunately, this test is skipped for now because there is no way to
// execute the whole test suite in mobile (it only works test by test), so
// as the client action include for mobile is rejected when we are not in
// mobile, it isn't possible to test it
// Moreover, RPCs done by the chat_manager (e.g. message_fetch) should be
// properly mocked.
assert.expect(11);
var parent = new Widget();
testUtils.addMockEnvironment(parent, {
data: this.data,
archs: {
'mail.message,false,search': '<search/>',
},
config: {device: {isMobile: true}},
});
var params = {
id: 1,
context: {},
params: {},
};
var discuss = createDiscuss(params);
// test basic rendering in mobile
assert.equal(discuss.$(".o_mail_chat_mobile_control_panel").length, 1, "Mobile control panel created");
assert.equal(discuss.$(".o_mail_mobile_tab").length, 4, "Four mobile tabs created");
assert.equal(discuss.$('.o_mail_chat_content').length, 1, "One default chat content pane created");
assert.equal(discuss.$(".o_mail_chat_tab_pane").length, 3, "Three mobile tab panes created");
// Inbox
assert.equal(discuss.activeMobileTab, "channel_inbox", "'channel_inbox' is default active tab");
assert.ok(discuss.$(".o_channel_inbox_item:nth(0)").hasClass("btn-primary"), "Showing 'Inbox'");
// Starred
discuss.$(".o_channel_inbox_item[data-type='channel_starred']").click();
assert.ok(discuss.$(".o_channel_inbox_item:nth(1)").hasClass("btn-primary"), "Clicked on 'Starred'");
assert.ok(discuss.$(".o_mail_chat_content").is(":visible"), "Default main content pane visible");
discuss.$(".o_mail_mobile_tab[data-type='dm']").click();
assert.equal(discuss.activeMobileTab, "dm", "After click on 'Conversation', is now active tab");
assert.ok(!discuss.$(".o_mail_chat_content").is(":visible"), "none", "'Main' content pane is invisible");
assert.ok(discuss.$(".o_mail_chat_tab_pane:nth(0)").is(":visible"), "'Conversation' pane is visible");
discuss.destroy();
});
});
});
@@ -0,0 +1,35 @@
odoo.define('mail.MockServer', function (require) {
"use strict";
var MockServer = require('web.MockServer');
MockServer.include({
/**
* @override
*/
_performRpc: function (route, args) {
if (route === '/mail/init_messaging') {
return $.when(this._mockInitMessaging(args));
}
return this._super(route, args);
},
/**
* Simulate the '/mail/init_messaging' route
*
* @private
* @return {Object}
*/
_mockInitMessaging: function () {
return {
'needaction_inbox_counter': 0,
'starred_counter': 0,
'channel_slots': [],
'commands': [],
'mention_partner_suggestions': [],
'shortcodes': [],
'menu_id': false,
};
},
});
});
@@ -0,0 +1,85 @@
odoo.define('mail.testUtils', function (require) {
"use strict";
var Discuss = require('mail.chat_discuss');
var AbstractService = require('web.AbstractService');
var Bus = require('web.Bus');
var testUtils = require('web.test_utils');
var Widget = require('web.Widget');
/**
* Test Utils
*
* In this module, we define some utility functions to create mock objects
* in the mail module, such as the BusService or Discuss.
*/
/**
* Create a mock bus_service, using 'bus' instead of bus.bus
*
* @param {web.bus} bus
* @return {AbstractService} the mock bus_service
*/
function createBusService(bus) {
var BusService = AbstractService.extend({
name: 'bus_service',
/**
* @override
*/
init: function (parent) {
this._super.apply(this, arguments);
if (!bus) {
bus = new Bus();
}
this.bus = new _.extend(bus, {
/**
* Do nothing
*/
start_polling: function () {},
});
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Get the bus
*/
getBus: function () {
return this.bus;
},
});
return BusService;
}
function createDiscuss(params) {
var Parent = Widget.extend({
do_push_state: function () {},
});
var parent = new Parent();
testUtils.addMockEnvironment(parent, {
data: params.data,
archs: {
'mail.message,false,search': '<search/>',
},
mockRPC: params.mockRPC,
services: params.services,
});
var discuss = new Discuss(parent, params);
discuss.set_cp_bus(new Widget());
discuss.appendTo($('#qunit-fixture'));
return discuss;
}
return {
createBusService: createBusService,
createDiscuss: createDiscuss,
};
});
+14 -8
View File
@@ -1,15 +1,19 @@
odoo.define('mail.systray_tests', function (require) {
"use strict";
var ChatManager = require('mail.ChatManager');
var systray = require('mail.systray');
var mailTestUtils = require('mail.testUtils');
var testUtils = require('web.test_utils');
var createBusService = mailTestUtils.createBusService;
QUnit.module('mail', {}, function () {
QUnit.module('ActivityMenu', {
beforeEach: function () {
this.BusService = createBusService();
this.data = {
'mail.activity.menu': {
fields: {
@@ -54,12 +58,13 @@ QUnit.test('activity menu widget: menu with no records', function (assert) {
var activityMenu = new systray.ActivityMenu();
testUtils.addMockEnvironment(activityMenu, {
mockRPC: function (route, args) {
if (args.method === 'activity_user_count') {
return $.when([]);
}
return this._super(route, args);
mockRPC: function (route, args) {
if (args.method === 'activity_user_count') {
return $.when([]);
}
return this._super(route, args);
},
services: [ChatManager, this.BusService]
});
activityMenu.appendTo($('#qunit-fixture'));
assert.ok(activityMenu.$('.o_no_activity').hasClass('o_no_activity'), "should not have instance of widget");
@@ -76,8 +81,9 @@ QUnit.test('activity menu widget: activity menu with 3 records', function (asser
return $.when(self.data['mail.activity.menu']['records']);
}
return this._super(route, args);
},
});
},
services: [ChatManager, this.BusService],
});
activityMenu.appendTo($('#qunit-fixture'));
assert.ok(activityMenu.$el.hasClass('o_mail_navbar_item'), 'should be the instance of widget');
assert.ok(activityMenu.$('.o_mail_channel_preview').hasClass('o_mail_channel_preview'), "should instance of widget");
+10 -8
View File
@@ -5,13 +5,14 @@
<xpath expr="." position="inside">
<script type="text/javascript" src="/mail/static/src/js/many2many_tags_email.js"></script>
<script type="text/javascript" src="/mail/static/src/js/client_action.js"></script>
<script type="text/javascript" src="/mail/static/src/js/client_action_mobile.js"></script>
<script type="text/javascript" src="/mail/static/src/js/chat_window.js"></script>
<script type="text/javascript" src="/mail/static/src/js/discuss.js"></script>
<script type="text/javascript" src="/mail/static/src/js/discuss_mobile.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/document_viewer.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/services/chat_manager.js"></script>
<script type="text/javascript" src="/mail/static/src/js/services/chat_window_manager.js"></script>
<script type="text/javascript" src="/mail/static/src/js/chatter.js"></script>
<script type="text/javascript" src="/mail/static/src/js/chatter_composer.js"></script>
<script type="text/javascript" src="/mail/static/src/js/followers.js"></script>
@@ -24,13 +25,11 @@
<script type="text/javascript" src="/mail/static/src/js/systray.js"></script>
<script type="text/javascript" src="/mail/static/src/js/tour.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>
<script type="text/javascript" src="/mail/static/src/js/activity.js"></script>
<script type="text/javascript" src="/mail/static/src/js/chat_mixin.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/discuss.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"/>
@@ -43,15 +42,18 @@
<template id="qunit_suite" name="mail_tests" inherit_id="web.qunit_suite">
<xpath expr="//t[@t-set='head']" position="inside">
<script type="text/javascript" src="/mail/static/tests/chat_window_tests.js"></script>
<script type="text/javascript" src="/mail/static/tests/chatter_tests.js"></script>
<script type="text/javascript" src="/mail/static/tests/discuss_tests.js"></script>
<script type="text/javascript" src="/mail/static/tests/systray_tests.js"></script>
<script type="text/javascript" src="/mail/static/tests/chat_window_test.js"></script>
<script type="text/javascript" src="/mail/static/tests/helpers/mock_server.js"></script>
<script type="text/javascript" src="/mail/static/tests/helpers/test_utils.js"></script>
</xpath>
</template>
<template id="qunit_mobile_suite" name="mail_mobile_tests" inherit_id="web.qunit_mobile_suite">
<xpath expr="//t[@t-set='head']" position="inside">
<script type="text/javascript" src="/mail/static/tests/client_action_mobile_tests.js"></script>
<script type="text/javascript" src="/mail/static/tests/discuss_mobile_tests.js"></script>
</xpath>
</template>
@@ -20,16 +20,16 @@ var data_manager = require('web.data_manager');
var Dialog = require('web.Dialog');
var dom = require('web.dom');
var Loading = require('web.Loading');
var mixins = require('web.mixins');
var NotificationManager = require('web.NotificationManager');
var RainbowMan = require('web.RainbowMan');
var ServiceProviderMixin = require('web.ServiceProviderMixin');
var session = require('web.session');
var Widget = require('web.Widget');
var _t = core._t;
var qweb = core.qweb;
var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
var AbstractWebClient = Widget.extend(ServiceProviderMixin, {
custom_events: {
clear_uncommitted_changes: function (e) {
this.clear_uncommitted_changes().then(e.data.callback);
@@ -90,8 +90,8 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
},
init: function (parent) {
this.client_options = {};
mixins.ServiceProvider.init.call(this);
this._super(parent);
ServiceProviderMixin.init.call(this);
this.origin = undefined;
this._current_state = null;
this.menu_dm = new concurrency.DropMisordered();
@@ -12,6 +12,7 @@ var crash_manager = require('web.crash_manager');
var framework = require('web.framework');
var session = require('web.session');
var _t = core._t;
var _lt = core._lt;
@@ -2,15 +2,22 @@ odoo.define('web.AbstractService', function (require) {
"use strict";
var Class = require('web.Class');
var Mixins = require('web.mixins');
var ServicesMixin = require('web.ServicesMixin');
var AbstractService = Class.extend({
var AbstractService = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
Services: [],
dependencies: [],
name: null,
init: function (parent) {
Mixins.EventDispatcherMixin.init.call(this, arguments);
this.setParent(parent);
},
});
var realExtend = AbstractService.extend;
AbstractService.extend = function() {
AbstractService.extend = function () {
var Service = realExtend.apply(this, arguments);
AbstractService.prototype.Services.push(Service);
return Service;
+2 -2
View File
@@ -10,8 +10,8 @@ var mixins = require('web.mixins');
* @class Bus
*/
return Class.extend(mixins.EventDispatcherMixin, {
init: function() {
mixins.EventDispatcherMixin.init.call(this, parent);
init: function () {
mixins.EventDispatcherMixin.init.call(this, arguments);
},
});
-125
View File
@@ -3,7 +3,6 @@ odoo.define('web.mixins', function (require) {
var Class = require('web.Class');
var utils = require('web.utils');
var AbstractService = require('web.AbstractService');
/**
* Mixin to structure objects' life-cycles folowing a parent-children
@@ -409,134 +408,10 @@ var PropertiesMixin = _.extend({}, EventDispatcherMixin, {
}
});
var ServiceProvider = {
services: {},
init: function () {
var self = this;
_.each(AbstractService.prototype.Services, function (Service) {
var service = new Service();
self.services[service.name] = service;
});
this.custom_events = _.clone(this.custom_events);
this.custom_events.call_service = this._call_service.bind(this);
},
_call_service: function (event) {
var service = this.services[event.data.service];
var args = (event.data.args || []).concat(event.target);
var result = service[event.data.method].apply(service, args);
event.data.callback(result);
},
};
return {
ParentedMixin: ParentedMixin,
EventDispatcherMixin: EventDispatcherMixin,
PropertiesMixin: PropertiesMixin,
ServiceProvider: ServiceProvider,
};
});
odoo.define('web.ServicesMixin', function (require) {
"use strict";
var rpc = require('web.rpc');
/**
* @mixin
* @name ServicesMixin
*/
var ServicesMixin = {
call: function (service, method) {
var args = Array.prototype.slice.call(arguments, 2);
var result;
this.trigger_up('call_service', {
service: service,
method: method,
args: args,
callback: function (r) {
result = r;
},
});
return result;
},
/**
* Builds and executes RPC query. Returns a deferred's promise resolved with
* the RPC result.
*
* @param {string} params either a route or a model
* @param {string} options if a model is given, this argument is a method
* @returns {Promise}
*/
_rpc: function (params, options) {
var query = rpc.buildQuery(params);
var def = this.call('ajax', 'rpc', query.route, query.params, options);
return def ? def.promise() : $.Deferred().promise();
},
loadFieldView: function (dataset, view_id, view_type, options) {
return this.loadViews(dataset.model, dataset.get_context().eval(), [[view_id, view_type]], options).then(function (result) {
return result[view_type];
});
},
loadViews: function (modelName, context, views, options) {
var def = $.Deferred();
this.trigger_up('load_views', {
modelName: modelName,
context: context,
views: views,
options: options,
on_success: def.resolve.bind(def),
});
return def;
},
loadFilters: function (dataset, action_id) {
var def = $.Deferred();
this.trigger_up('load_filters', {
dataset: dataset,
action_id: action_id,
on_success: def.resolve.bind(def),
});
return def;
},
// Session stuff
getSession: function () {
var session;
this.trigger_up('get_session', {
callback: function (result) {
session = result;
}
});
return session;
},
/**
* Informs the action manager to do an action. This supposes that the action
* manager can be found amongst the ancestors of the current widget.
* If that's not the case this method will simply return an unresolved
* deferred.
*
* @param {any} action
* @param {any} options
* @returns {Deferred}
*/
do_action: function (action, options) {
var def = $.Deferred();
this.trigger_up('do_action', {
action: action,
options: options,
on_success: function (result) { def.resolve(result); },
on_fail: function (result) { def.reject(result); },
});
return def;
},
do_notify: function (title, message, sticky, className) {
this.trigger_up('notification', {title: title, message: message, sticky: sticky, className: className});
},
do_warn: function (title, message, sticky, className) {
this.trigger_up('warning', {title: title, message: message, sticky: sticky, className: className});
},
};
return ServicesMixin;
});
@@ -0,0 +1,164 @@
odoo.define('web.ServiceProviderMixin', function (require) {
"use strict";
var AbstractService = require('web.AbstractService');
var utils = require('web.utils');
var ServiceProviderMixin = {
services: {},
init: function () {
// to properly instantiate services with this as parent, this mixin
// assumes that it is used along the EventDispatcherMixin, and that
// EventDispatchedMixin's init is called first
var self = this;
// as EventDispatcherMixin's init is already called, this handler has to
// be bound manually
this.on('call_service', this, this._call_service.bind(this));
var sortedServices = this._sortServices(AbstractService.prototype.Services);
_.each(sortedServices, function (Service) {
var service = new Service(self);
self.services[service.name] = service;
});
},
_call_service: function (event) {
var service = this.services[event.data.service];
var args = event.data.args || [];
// ajax service uses an extra 'target' argument for rpc
if (event.data.service === 'ajax' && event.data.method === 'rpc') {
args = args.concat(event.target);
}
var result = service[event.data.method].apply(service, args);
event.data.callback(result);
},
_sortServices: function (services) {
var nodes = {};
// Create nodes (services)
_.each(services, function (Service) {
nodes[Service.prototype.name] = Service.prototype.dependencies;
});
var sorted;
try {
sorted = utils.topologicalSort(nodes);
} catch (err) {
console.warn('topologicalSort Error:', err.message);
sorted = nodes;
}
// Sort services based on sorted
// Note: we convert sorted to an object key=>index for efficiency
var sortedObj = _.invert(_.object(_.pairs(sorted)));
sorted = _.sortBy(services, function (Service) {
return sortedObj[Service.prototype.name];
});
return sorted;
},
};
return ServiceProviderMixin;
});
odoo.define('web.ServicesMixin', function (require) {
"use strict";
var rpc = require('web.rpc');
/**
* @mixin
* @name ServicesMixin
*/
var ServicesMixin = {
call: function (service, method) {
var args = Array.prototype.slice.call(arguments, 2);
var result;
this.trigger_up('call_service', {
service: service,
method: method,
args: args,
callback: function (r) {
result = r;
},
});
return result;
},
/**
* Builds and executes RPC query. Returns a deferred's promise resolved with
* the RPC result.
*
* @param {string} params either a route or a model
* @param {string} options if a model is given, this argument is a method
* @returns {Promise}
*/
_rpc: function (params, options) {
var query = rpc.buildQuery(params);
var def = this.call('ajax', 'rpc', query.route, query.params, options);
return def ? def.promise() : $.Deferred().promise();
},
loadFieldView: function (dataset, view_id, view_type, options) {
return this.loadViews(dataset.model, dataset.get_context().eval(), [[view_id, view_type]], options).then(function (result) {
return result[view_type];
});
},
loadViews: function (modelName, context, views, options) {
var def = $.Deferred();
this.trigger_up('load_views', {
modelName: modelName,
context: context,
views: views,
options: options,
on_success: def.resolve.bind(def),
});
return def;
},
loadFilters: function (dataset, action_id) {
var def = $.Deferred();
this.trigger_up('load_filters', {
dataset: dataset,
action_id: action_id,
on_success: def.resolve.bind(def),
});
return def;
},
// Session stuff
getSession: function () {
var session;
this.trigger_up('get_session', {
callback: function (result) {
session = result;
}
});
return session;
},
/**
* Informs the action manager to do an action. This supposes that the action
* manager can be found amongst the ancestors of the current widget.
* If that's not the case this method will simply return an unresolved
* deferred.
*
* @param {any} action
* @param {any} options
* @returns {Deferred}
*/
do_action: function (action, options) {
var def = $.Deferred();
this.trigger_up('do_action', {
action: action,
options: options,
on_success: function (result) { def.resolve(result); },
on_fail: function (result) { def.reject(result); },
});
return def;
},
do_notify: function (title, message, sticky, className) {
this.trigger_up('notification', {title: title, message: message, sticky: sticky, className: className});
},
do_warn: function (title, message, sticky, className) {
this.trigger_up('warning', {title: title, message: message, sticky: sticky, className: className});
},
};
return ServicesMixin;
});
+31
View File
@@ -300,6 +300,37 @@ var utils = {
}
return fn(mod, Math.floor(x));
},
/**
* Topological sort of nodes
*
* @param {Object} nodes where key is name of node, and value is list of
* names of dependent nodes.
* @param {string[]} nodes[name] list of named dependencies of node having
* 'name' as its name.
* @return {string[]} topological sort of nodes by their names.
* @throws {Error} if there is at least one circular
* dependency between nodes
*/
topologicalSort: function (nodes) {
var sorted = [];
var unresolved = nodes;
while (!_.isEmpty(unresolved)) {
// Get all nodes that have no dependency
var resolvable = _.pick(unresolved, function (dependencies, name) {
return _.isEmpty(_.difference(dependencies, sorted));
});
if (_.isEmpty(resolvable)) {
throw new Error("Circular dependency detected");
}
// Append them to sorted
sorted = _.union(sorted, _.keys(resolvable));
// Remove these nodes from unresolved
_.each(resolvable, function (dependencies, name) {
unresolved = _.omit(unresolved, name);
});
}
return sorted;
},
/**
* performs a half up rounding with a fixed amount of decimals, correcting for float loss of precision
* See the corresponding float_round() in server/tools/float_utils.py for more info
@@ -6,12 +6,10 @@ odoo.define('web.ReportService', function (require) {
*/
var AbstractService = require('web.AbstractService');
var session = require('web.session');
var wkhtmltopdfState;
var ReportService = AbstractService.extend({
name: 'report',
dependencies: ['ajax'],
/**
* Checks the state of the installation of wkhtmltopdf on the server.
@@ -21,12 +19,12 @@ var ReportService = AbstractService.extend({
* (possible values are 'ok', 'broken', 'install', 'upgrade', 'workers').
*/
checkWkhtmltopdf: function () {
if (!wkhtmltopdfState) {
// AAB: for now, services aren't available for each other, so we
// can't use this._rpc (the ajax service)
wkhtmltopdfState = session.rpc('/report/check_wkhtmltopdf');
if (!this.wkhtmltopdfState) {
this.wkhtmltopdfState = this._rpc({
route:'/report/check_wkhtmltopdf'
});
}
return wkhtmltopdfState;
return this.wkhtmltopdfState;
},
});
@@ -1,9 +1,11 @@
odoo.define('web.action_manager_tests', function (require) {
"use strict";
var ReportClientAction = require('report.client_action');
var ControlPanelMixin = require('web.ControlPanelMixin');
var core = require('web.core');
var ReportClientAction = require('report.client_action');
var ReportService = require('web.ReportService');
var testUtils = require('web.test_utils');
var Widget = require('web.Widget');
@@ -1338,6 +1340,7 @@ QUnit.module('ActionManager', {
}
return this._super.apply(this, arguments);
},
services: [ReportService],
session: {
get_file: function (params) {
assert.step(params.url);
@@ -1347,7 +1350,9 @@ QUnit.module('ActionManager', {
},
});
actionManager.doAction(7, {
on_close: assert.step.bind(assert, 'on_close'),
on_close: function () {
assert.step('on_close');
},
});
assert.verifySteps([
@@ -1374,6 +1379,7 @@ QUnit.module('ActionManager', {
}
return this._super.apply(this, arguments);
},
services: [ReportService],
session: {
get_file: function (params) {
assert.step(params.url);
@@ -1388,7 +1394,6 @@ QUnit.module('ActionManager', {
},
});
actionManager.doAction(7);
assert.verifySteps([
'/web/action/load',
'/report/check_wkhtmltopdf',
@@ -1430,6 +1435,7 @@ QUnit.module('ActionManager', {
}
return this._super.apply(this, arguments);
},
services: [ReportService],
session: {
get_file: function (params) {
assert.step(params.url); // should not be called
@@ -430,7 +430,7 @@ var MockServer = Class.extend({
return result;
},
/**
* Simulate a 'field_get' operation
* Simulate a 'fields_get' operation
*
* @private
* @param {string} modelName
@@ -464,7 +464,7 @@ var MockServer = Class.extend({
if (!action) {
// when the action doesn't exist, the real server doesn't crash, it
// simply returns false
console.warn("No action found for ID " + event.data.actionID);
console.warn("No action found for ID " + kwargs.action_id);
}
return action || false;
},
+48 -10
View File
@@ -19,6 +19,7 @@ var core = require('web.core');
var dom = require('web.dom');
var session = require('web.session');
var MockServer = require('web.MockServer');
var utils = require('web.utils');
var Widget = require('web.Widget');
var DebouncedField = basic_fields.DebouncedField;
@@ -97,12 +98,6 @@ var createActionManager = function (params) {
},
});
addMockEnvironment(widget, _.defaults(params, {debounce: false}));
intercept(widget, 'call_service', function (event) {
if (event.data.service === 'report') {
var state = widget._rpc({route: '/report/check_wkhtmltopdf'});
event.data.callback(state);
}
}, true);
widget.appendTo($target);
widget.$el.addClass('o_web_client');
@@ -330,6 +325,7 @@ function addMockEnvironment(widget, params) {
currentDate: params.currentDate,
debug: params.debug,
});
// make sure the debounce value for input fields is set to 0
var initialDebounceValue = DebouncedField.prototype.DEBOUNCE;
DebouncedField.prototype.DEBOUNCE = params.fieldDebounce || 0;
@@ -406,11 +402,53 @@ function addMockEnvironment(widget, params) {
widgetDestroy.call(this);
};
intercept(widget, 'call_service', function (event) {
if (event.data.service === 'ajax') {
var result = mockServer.performRpc(event.data.args[0], event.data.args[1]);
event.data.callback(result);
// Dispatch service calls
// Note: some services could call other services at init,
// Which is why we have to init services after that
var services = {};
intercept(widget, 'call_service', function (ev) {
var args, result;
if (ev.data.service === 'ajax') {
// ajax service is already mocked by the server
var route = ev.data.args[0];
args = ev.data.args[1];
result = mockServer.performRpc(route, args);
} else if (services[ev.data.service]) {
var service = services[ev.data.service];
args = (ev.data.args || []);
result = service[ev.data.method].apply(service, args);
}
ev.data.callback(result);
});
// Instantiate services
// Note: ensure topological sort of services based on their dependencies
var sortServices = function (services) {
// Create nodes (services), with ajax already loaded
var nodes = { ajax: [] };
_.each(services, function (Service) {
nodes[Service.prototype.name] = Service.prototype.dependencies;
});
var sorted;
try {
sorted = utils.topologicalSort(nodes);
} catch (err) {
console.warn('topologicalSort Error:', err.message);
sorted = nodes;
}
// Remove ajax from sorted
sorted = _.without(sorted, 'ajax');
// Sort services based on sorted
// Note: we convert sorted to an object key=>index for efficiency
var sortedObj = _.invert(_.object(_.pairs(sorted)));
sorted = _.sortBy(services, function (Service) {
return sortedObj[Service.prototype.name];
});
return sorted;
};
var sortedServices = sortServices(params.services);
_.each(sortedServices, function (Service) {
var service = new Service(widget);
services[service.name] = service;
});
intercept(widget, 'load_action', function (event) {
+1
View File
@@ -76,6 +76,7 @@
<script type="text/javascript" src="/web/static/src/js/services/ajax_service.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/time.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/mixins.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/service_mixins.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/rpc.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/widget.js"></script>
<script type="text/javascript" src="/web/static/src/js/core/registry.js"></script>
@@ -1,9 +1,9 @@
odoo.define('web_editor.BodyManager', function (require) {
'use strict';
var mixins = require('web.mixins');
var session = require('web.session');
var rootWidget = require('web_editor.root_widget');
var ServiceProviderMixin = require('web.ServiceProviderMixin');
var session = require('web.session');
/**
* Element which is designed to be unique and that will be the top-most element
@@ -11,13 +11,13 @@ var rootWidget = require('web_editor.root_widget');
* this Class instance. Its main role will be to retrieve RPC demands from its
* children and handle them.
*/
var BodyManager = rootWidget.RootWidget.extend(mixins.ServiceProvider, {
var BodyManager = rootWidget.RootWidget.extend(ServiceProviderMixin, {
/**
* @constructor
*/
init: function () {
mixins.ServiceProvider.init.call(this);
this._super.apply(this, arguments);
ServiceProviderMixin.init.call(this);
},
/**
* @override
@@ -7,6 +7,7 @@ var config = require('web.config');
var core = require('web.core');
var mixins = require('web.mixins');
var rpc = require('web.rpc');
var ServiceProviderMixin = require('web.ServiceProviderMixin');
var session = require('web.session');
var TourManager = require('web_tour.TourManager');
@@ -16,10 +17,10 @@ if (config.device.isMobile) {
return $.Deferred().reject();
}
var CallService = Class.extend(mixins.EventDispatcherMixin, mixins.ServiceProvider, {
var CallService = Class.extend(mixins.EventDispatcherMixin, ServiceProviderMixin, {
init: function () {
mixins.ServiceProvider.init.call(this);
mixins.EventDispatcherMixin.init.call(this);
ServiceProviderMixin.init.call(this);
},
});
@@ -9,6 +9,7 @@
<template id="assets_frontend" name="im_livechat assets frontend" inherit_id="website.assets_frontend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/bus/static/src/js/bus.js"></script>
<script type="text/javascript" src="/bus/static/src/js/services/bus_service.js"></script>
<script type="text/javascript" src="/mail/static/src/js/chat_window.js"></script>
<script type="text/javascript" src="/mail/static/src/js/document_viewer.js"></script>
<script type="text/javascript" src="/mail/static/src/js/thread.js"></script>