[REF][FIX] mail, im_livechat: various improvements (JS)
This commit makes a lot of small improvements of the JS of mail and
related modules.
A. Mail Manager
---------------
im_support:
- [REF] Slightly more robust way to determine a support
message, so that it more reliably falls back on
default kind of message (i.e. support channel UUID
should be clearly set).
mail:
- [REF] Improved method names for readability purposes.
Some examples:
- `fetchFromServer` becomes `fetchMailStateFromServer`
- `_initialize{*}` becomes `_update{*}FromServer`
- `_isModerator` becomes `_isMyselfModerator`
- [REF] Use 'template' pattern design to make Include on
Mail Manager much more robust. In particular,
the flow is always as follow:
1. Initialize internal state.
2. Listen on buses.
3. Fetch mail data from server.
- [REF] Improved workflow for thread window workflow
(Shorter & more readable).
B. Mail Tests
-------------
- [REF] Thread Window Tests crashes with clean message when
there are some thread windows open before running the
tests.
C. (Model) Message
------------------
- [REF] Improved method names for readability purposes
(e.g. `isAuthor` becomes `isMyselfAuthor`).
D. (Model) Thread
-----------------
- [REF] Improved `init` parameters on each kind of thread.
- [REF] 'Thread With Cache' has been renamed to 'Searchable
Thread': it embraces all kinds of threads that can
be used with the search view.
- [REF] 'DM' has been renamed to 'DM Chat' (for clarity).
- [IMP] New Livechat model in the backend.
- [REF] No more direct instances of the Channel class: public
and private channels are now 'Multi-User' channels,
while DM Chat and Livechat are 'Two-User' channels:
This change has been made so that instances of classes
always have direct classes as "leaf" in the Class
modeling. It eases making changes to a certain model
without being too constrained by another model.
- [REF] As a consequence of above changed, the method `isChat`
has been renamed to `isTwoUserThread`.
This is to avoid confusion with the terminology
'chat', which is more like a shortcut for 'quick real-
time conversation'. In other words, the property of
'chat' depends of the way to interact on a thread, not
of the intrinsic type of the thread.
Still, some parts have kept the terminology 'chat',
such as in the systray messagin menu.
- [IMP] Added `message_added` and `message_posted` hooks,
which may be useful to easily hook on those events.
E. Thread Widget
----------------
- [REF] Now uses a `mail.model.AbstractThread` for the
rendering of a thread, instead of a list of
`{mail.model.AbstractMessage}`.
- [FIX] Stick thread scroll height to bottom on new rendering,
if it was previously at the bottom of the thread.
- [REF] Option ORDER has its meaning changed: it now refers
to the chronological order of messages in the thread,
in which DESC means 'from top to bottom', while ASC
means 'from bottom to top'.
- [REF] Improved doc on templates, in particular with the
option `ORDER`.
F. Thread Window
----------------
- [REF] Moved more logic to thread windows, which were
previously in Mail Window Manager.
- [REF] Now hides the thread widget attribute. So instead of
`threadWindow.threadWidget.isAtBottom()`, it uses
`threadWindow.isAtBottom()`.
- [FIX] New received messages when not having focus on Odoo
tab should always make the corresponding thread
windows "passive" (i.e. windows that keep threads
unread until click on them).
- [FIX] Blank thread window (i.e. 'New message' TW) correctly
opens Discuss with 'Inbox' when clicking on 'Expand'
Button in the title
- [FIX] Do not display the composer for channels with property
'mass-mailing' set in thread windows.
G. (Website) Livechat
---------------------
- [REF] Some `trigger` have been turned into `trigger_up`.
- [REF] Some logic has been moved to models (e.g.
`{im_livechat.model.WebsiteLivechat}`), from
`{im_livechat.im_livechat.LivechatButton}`.
- [FIX] New messages should be received in real-time. The
longpolling was broken with a recent refactoring on
bus service (6448420c5d).
This commit is contained in:
@@ -2,7 +2,6 @@ odoo.define('bus.CrossTab', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Longpolling = require('bus.Longpolling');
|
||||
var session = require('web.session');
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,6 +44,12 @@ if (!_.contains(urlHistory, page)) {
|
||||
|
||||
var LivechatButton = Widget.extend({
|
||||
className:'openerp o_livechat_button d-print-none',
|
||||
custom_events: {
|
||||
'close_chat_window': '_onCloseChatWindow',
|
||||
'post_message_chat_window': '_onPostMessageChatWindow',
|
||||
'save_chat_window': '_onSaveChatWindow',
|
||||
'updated_unread_counter': '_onUpdatedUnreadCounter',
|
||||
},
|
||||
events: {
|
||||
'click': '_openChat'
|
||||
},
|
||||
@@ -112,7 +118,7 @@ var LivechatButton = Widget.extend({
|
||||
* @param {Object} [options={}]
|
||||
*/
|
||||
_addMessage: function (data, options) {
|
||||
options = _.extend({}, options, {
|
||||
options = _.extend({}, this.options, options, {
|
||||
serverURL: this._serverURL,
|
||||
});
|
||||
var message = new WebsiteLivechatMessage(this, data, options);
|
||||
@@ -130,7 +136,7 @@ var LivechatButton = Widget.extend({
|
||||
this._chatWindow.$('.o_thread_composer input').prop('disabled', true);
|
||||
|
||||
var feedback = new Feedback(this, this._livechat);
|
||||
feedback.replace(this._chatWindow.threadWidget.$el);
|
||||
this._chatWindow.replaceContentWith(feedback);
|
||||
|
||||
feedback.on('send_message', this, this._sendMessage);
|
||||
feedback.on('feedback_sent', this, this._closeChat);
|
||||
@@ -159,7 +165,7 @@ var LivechatButton = Widget.extend({
|
||||
} else { // normal message
|
||||
this._addMessage(notification[1]);
|
||||
this._renderMessages();
|
||||
if (this._chatWindow.isFolded() || !this._chatWindow.threadWidget.isAtBottom()) {
|
||||
if (this._chatWindow.isFolded() || !this._chatWindow.isAtBottom()) {
|
||||
this._livechat.incrementUnreadCounter();
|
||||
}
|
||||
}
|
||||
@@ -204,13 +210,16 @@ var LivechatButton = Widget.extend({
|
||||
if (!livechatData || !livechatData.operator_pid) {
|
||||
alert(_t("None of our collaborators seems to be available, please try again later."));
|
||||
} else {
|
||||
self._livechat = new WebsiteLivechat(livechatData);
|
||||
self._livechat = new WebsiteLivechat({
|
||||
parent: self,
|
||||
data: livechatData
|
||||
});
|
||||
self._openChatWindow();
|
||||
self._sendWelcomeMessage();
|
||||
self._renderMessages();
|
||||
|
||||
self.busBus.addChannel(self._livechat.getUUID());
|
||||
self.busBus.startPolling();
|
||||
self.call('bus_service', 'addChannel', self._livechat.getUUID());
|
||||
self.call('bus_service', 'startPolling');
|
||||
|
||||
utils.set_cookie('im_livechat_session', JSON.stringify(self._livechat.toData()), 60*60);
|
||||
utils.set_cookie('im_livechat_auto_popup', JSON.stringify(false), 60*60);
|
||||
@@ -235,41 +244,16 @@ var LivechatButton = Widget.extend({
|
||||
self._chatWindow.$el.css(cssProps);
|
||||
self.$el.hide();
|
||||
});
|
||||
this._chatWindow.on('close', this, function () {
|
||||
var input_disabled = this._chatWindow.$('.o_thread_composer input').prop('disabled');
|
||||
var ask_fb = !input_disabled && _.find(this._messages, function (message) {
|
||||
return message.getID() !== '_welcome';
|
||||
});
|
||||
if (ask_fb) {
|
||||
this._chatWindow.toggleFold(false);
|
||||
this._askFeedback();
|
||||
} else {
|
||||
this._closeChat();
|
||||
}
|
||||
});
|
||||
this._chatWindow.on('post_message', this, function (message) {
|
||||
self._sendMessage(message).fail(function (error, e) {
|
||||
e.preventDefault();
|
||||
return self._sendMessage(message); // try again just in case
|
||||
});
|
||||
});
|
||||
this._chatWindow.on('save_chat', this, this._onSaveChat);
|
||||
this._chatWindow.threadWidget.$el.on('scroll', null, _.debounce(function () {
|
||||
if (self._chatWindow.threadWidget.isAtBottom()) {
|
||||
self._livechat.resetUnreadCounter();
|
||||
self._chatWindow.renderHeader();
|
||||
}
|
||||
}, 100));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_renderMessages: function () {
|
||||
var shouldScroll = !this._chatWindow.isFolded() && this._chatWindow.threadWidget.isAtBottom();
|
||||
var shouldScroll = !this._chatWindow.isFolded() && this._chatWindow.isAtBottom();
|
||||
this._livechat.setMessages(this._messages);
|
||||
this._chatWindow.render();
|
||||
if (shouldScroll) {
|
||||
this._chatWindow.threadWidget.scrollToBottom();
|
||||
this._chatWindow.scrollToBottom();
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -282,7 +266,7 @@ var LivechatButton = Widget.extend({
|
||||
return session
|
||||
.rpc('/mail/chat_post', {uuid: this._livechat.getUUID(), message_content: message.content})
|
||||
.then(function () {
|
||||
self._chatWindow.threadWidget.scrollToBottom();
|
||||
self._chatWindow.scrollToBottom();
|
||||
});
|
||||
},
|
||||
/**
|
||||
@@ -306,6 +290,23 @@ var LivechatButton = Widget.extend({
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onCloseChatWindow: function (ev) {
|
||||
ev.stopPropagation();
|
||||
var isComposerDisabled = this._chatWindow.$('.o_thread_composer input').prop('disabled');
|
||||
var shouldAskFeedback = !isComposerDisabled && _.find(this._messages, function (message) {
|
||||
return message.getID() !== '_welcome';
|
||||
});
|
||||
if (shouldAskFeedback) {
|
||||
this._chatWindow.toggleFold(false);
|
||||
this._askFeedback();
|
||||
} else {
|
||||
this._closeChat();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Array[]} notifications
|
||||
@@ -318,10 +319,34 @@ var LivechatButton = Widget.extend({
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
* @param {Object} ev.data.messageData
|
||||
*/
|
||||
_onSaveChat: function () {
|
||||
_onPostMessageChatWindow: function (ev) {
|
||||
ev.stopPropagation();
|
||||
var self = this;
|
||||
var messageData = ev.data.messageData;
|
||||
this._sendMessage(messageData).fail(function (error, e) {
|
||||
e.preventDefault();
|
||||
return self._sendMessage(messageData); // try again just in case
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onSaveChatWindow: function (ev) {
|
||||
ev.stopPropagation();
|
||||
utils.set_cookie('im_livechat_session', JSON.stringify(this._livechat.toData()), 60*60);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onUpdatedUnreadCounter: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this._chatWindow.renderHeader();
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
@@ -3,6 +3,8 @@ odoo.define('im_livechat.model.WebsiteLivechat', function (require) {
|
||||
|
||||
var AbstractThread = require('mail.model.AbstractThread');
|
||||
|
||||
var session = require('web.session');
|
||||
|
||||
/**
|
||||
* Thread model that represents a livechat on the website-side. This livechat
|
||||
* is not linked to the mail service.
|
||||
@@ -12,34 +14,37 @@ var WebsiteLivechat = AbstractThread.extend({
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
* @param {Object} livechatData
|
||||
* @param {boolean} [livechatData.folded] states whether the livechat is
|
||||
* @param {Object} params
|
||||
* @param {Object} params.data
|
||||
* @param {string} params.data.anonymous_name
|
||||
* @param {boolean} [params.data.folded] states whether the livechat is
|
||||
* folded or not. It is considered only if this is defined and it is a
|
||||
* boolean.
|
||||
* @param {integer} livechatData.id the ID of this livechat.
|
||||
* @param {integer} [livechatData.message_unread_counter] the
|
||||
* unread counter of this livechat.
|
||||
* @param {Array} livechatData.operator_pid
|
||||
* @param {string} livechatData.name the name of this livechat.
|
||||
* @param {string} [livechatData.state] if 'folded', the livechat is folded.
|
||||
* @param {integer} params.data.id the ID of this livechat.
|
||||
* @param {integer} [params.data.message_unread_counter] the unread counter
|
||||
* of this livechat.
|
||||
* @param {Array} params.data.operator_pid
|
||||
* @param {string} params.data.name the name of this livechat.
|
||||
* @param {string} [params.data.state] if 'folded', the livechat is folded.
|
||||
* This is ignored if `folded` is provided and is a boolean value.
|
||||
* @param {string} livechatData.uuid the UUID of this livechat.
|
||||
* @param {string} params.data.uuid the UUID of this livechat.
|
||||
* @param {im_livechat.im_livechat.LivechatButton} params.parent
|
||||
*/
|
||||
init: function (livechatData) {
|
||||
var params = { data: livechatData };
|
||||
this._super.call(this, params);
|
||||
init: function (params) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this._operatorPID = livechatData.operator_pid;
|
||||
this._uuid = livechatData.uuid;
|
||||
this._members = [];
|
||||
this._operatorPID = params.data.operator_pid;
|
||||
this._uuid = params.data.uuid;
|
||||
|
||||
if (livechatData.message_unread_counter !== undefined) {
|
||||
this._unreadCounter = livechatData.message_unread_counter;
|
||||
if (params.data.message_unread_counter !== undefined) {
|
||||
this._unreadCounter = params.data.message_unread_counter;
|
||||
}
|
||||
|
||||
if (_.isBoolean(livechatData.folded)) {
|
||||
this._folded = livechatData.folded;
|
||||
if (_.isBoolean(params.data.folded)) {
|
||||
this._folded = params.data.folded;
|
||||
} else {
|
||||
this._folded = livechatData.state === 'folded';
|
||||
this._folded = params.data.state === 'folded';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -66,6 +71,16 @@ var WebsiteLivechat = AbstractThread.extend({
|
||||
getUUID: function () {
|
||||
return this._uuid;
|
||||
},
|
||||
/**
|
||||
* Increments the unread counter of this livechat by 1 unit.
|
||||
*
|
||||
* Note: this public method makes sense because the management of messages
|
||||
* for website livechat is external. This method should be dropped when
|
||||
* this class handles messages by itself.
|
||||
*/
|
||||
incrementUnreadCounter: function () {
|
||||
this._incrementUnreadCounter();
|
||||
},
|
||||
/**
|
||||
* AKU: hack for the moment
|
||||
*
|
||||
@@ -87,6 +102,20 @@ var WebsiteLivechat = AbstractThread.extend({
|
||||
uuid: this.getUUID(),
|
||||
};
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Warn that the unread counter has been updated on this livechat
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_warnUpdatedUnreadCounter: function () {
|
||||
this.trigger_up('updated_unread_counter');
|
||||
},
|
||||
});
|
||||
|
||||
return WebsiteLivechat;
|
||||
|
||||
@@ -13,7 +13,7 @@ var AbstractMessage = require('mail.model.AbstractMessage');
|
||||
var WebsiteLivechatMessage = AbstractMessage.extend({
|
||||
|
||||
/**
|
||||
* @param {Widget} parent
|
||||
* @param {im_livechat.im_livechat.LivechatButton} parent
|
||||
* @param {Object} data
|
||||
* @param {Object} options
|
||||
* @param {string} options.serverURL
|
||||
|
||||
@@ -11,7 +11,7 @@ var AbstractThreadWindow = require('mail.AbstractThreadWindow');
|
||||
var LivechatWindow = AbstractThreadWindow.extend({
|
||||
/**
|
||||
* @override
|
||||
* @param parent
|
||||
* @param {im_livechat.im_livechat.LivechatButton} parent
|
||||
* @param {im_livechat.model.WebsiteLivechat} thread
|
||||
*/
|
||||
init: function (parent, thread) {
|
||||
@@ -28,7 +28,15 @@ var LivechatWindow = AbstractThreadWindow.extend({
|
||||
* @override
|
||||
*/
|
||||
close: function () {
|
||||
this.trigger('close');
|
||||
this.trigger_up('close_chat_window');
|
||||
},
|
||||
/**
|
||||
* Replace the thread content with provided new content
|
||||
*
|
||||
* @param {$.Element} $element
|
||||
*/
|
||||
replaceContentWith: function ($element) {
|
||||
$element.replace(this._threadWidget.$el);
|
||||
},
|
||||
/**
|
||||
* Warn the parent widget (LivechatButton)
|
||||
@@ -38,7 +46,7 @@ var LivechatWindow = AbstractThreadWindow.extend({
|
||||
*/
|
||||
toggleFold: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.trigger('save_chat');
|
||||
this.trigger_up('save_chat_window');
|
||||
this.updateVisualFoldState();
|
||||
},
|
||||
|
||||
@@ -52,7 +60,8 @@ var LivechatWindow = AbstractThreadWindow.extend({
|
||||
* @param {Object} messageData
|
||||
*/
|
||||
_postMessage: function (messageData) {
|
||||
this.trigger('post_message', messageData);
|
||||
this.trigger_up('post_message_chat_window', { messageData: messageData });
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -186,7 +186,10 @@ MailManager.include({
|
||||
* @override
|
||||
*/
|
||||
_makeMessage: function (data) {
|
||||
if (_.contains(data.channel_ids, this.supportChannelUUID)) {
|
||||
if (
|
||||
this.supportChannelUUID &&
|
||||
_.contains(data.channel_ids, this.supportChannelUUID)
|
||||
) {
|
||||
return new SupportMessage(this, data, this._emojis);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
@@ -239,10 +242,10 @@ MailManager.include({
|
||||
// ignore history requests
|
||||
return;
|
||||
}
|
||||
var message = _.extend(notification[1], {
|
||||
var messageData = _.extend(notification[1], {
|
||||
channel_ids: [self.supportChannelUUID],
|
||||
});
|
||||
self._handleChannelNotification(message);
|
||||
self._handleChannelNotification({ data: messageData });
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ odoo.define('im_support.SupportChannel', function (require) {
|
||||
|
||||
var supportSession = require('im_support.SupportSession');
|
||||
|
||||
var ThreadWithCache = require('mail.model.ThreadWithCache');
|
||||
var SearchableThread = require('mail.model.SearchableThread');
|
||||
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
@@ -17,9 +17,9 @@ var _t = core._t;
|
||||
* communicate from their respective 'backend' access.
|
||||
*
|
||||
* FIXME: it should inherit from mail.model.Channel, not from
|
||||
* mail.model.ThreadWithCache
|
||||
* mail.model.SearchableThread
|
||||
*/
|
||||
var SupportChannel = ThreadWithCache.extend({
|
||||
var SupportChannel = SearchableThread.extend({
|
||||
|
||||
/**
|
||||
* @override
|
||||
@@ -51,7 +51,7 @@ var SupportChannel = ThreadWithCache.extend({
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
// force stuff that should probably be in Thread (or at least
|
||||
// ThreadWithCache), but that are currently in Channel
|
||||
// SearchableThread), but that are currently in Channel
|
||||
this._detached = data.is_minimized;
|
||||
this._folded = data.state === 'folded';
|
||||
this._uuid = data.uuid;
|
||||
@@ -100,6 +100,22 @@ var SupportChannel = ThreadWithCache.extend({
|
||||
var value = folded ? 'folded' : 'open';
|
||||
this.call('mail_service', 'updateSupportChannelState', value);
|
||||
},
|
||||
/**
|
||||
* FIXME: this override is necessary just because the support channel is
|
||||
* considered as a channel, even though it does not inherit from
|
||||
* mail.model.Channel.
|
||||
*
|
||||
* @returns {integer}
|
||||
*/
|
||||
getNeedactionCounter: function () {
|
||||
return 0;
|
||||
},
|
||||
/**
|
||||
* @return {string} uuid of this channel
|
||||
*/
|
||||
getUUID: function () {
|
||||
return this._uuid;
|
||||
},
|
||||
/**
|
||||
* FIXME: this method is necessary just because the support channel is
|
||||
* considered as a channel, even though it does not inherit from
|
||||
@@ -128,44 +144,31 @@ var SupportChannel = ThreadWithCache.extend({
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* Support channels are not considered as chat, so they are not displayed
|
||||
* in the 'chat' filter of the systray messaging menu.
|
||||
* Called when fold or detach (or both) status have changed on the support
|
||||
* channel.
|
||||
*
|
||||
* @override
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isChat: function () {
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* FIXME: this override is necessary just because the support channel is
|
||||
* considered as a channel, even though it does not inherit from
|
||||
* mail.model.Channel.
|
||||
* Note: this is partially a hack due to support channel not being a
|
||||
* channel. Also, the support channel uses another way to handle its window
|
||||
* state, by means of the local storage.
|
||||
*
|
||||
* @returns {integer}
|
||||
* @param {Object} params
|
||||
* @param {boolean} [params.folded]
|
||||
* @param {boolean} [params.detached]
|
||||
*/
|
||||
getNeedactionCounter: function () {
|
||||
return 0;
|
||||
},
|
||||
/**
|
||||
* @return {string} uuid of this channel
|
||||
*/
|
||||
getUUID: function () {
|
||||
return this._uuid;
|
||||
},
|
||||
/**
|
||||
* Posts the message on the Support server.
|
||||
*
|
||||
* @override
|
||||
* @return {$.Promise}
|
||||
*/
|
||||
postMessage: function (data) {
|
||||
// ensure that the poll is active before posting the message
|
||||
this.call('mail_service', 'startPollingSupport');
|
||||
return supportSession.rpc('/odoo_im_support/chat_post', {
|
||||
uuid: this._supportChannelUUID,
|
||||
message_content: data.content,
|
||||
});
|
||||
updateWindowState: function (params) {
|
||||
if ('detached' in params) {
|
||||
this._detached = params.detached;
|
||||
}
|
||||
if ('folded' in params) {
|
||||
this._folded = params.folded;
|
||||
}
|
||||
|
||||
if (!this._detached) {
|
||||
this.call('mail_service', 'updateSupportChannelState', 'closed');
|
||||
} else {
|
||||
var value = this._folded ? 'folded' : 'open';
|
||||
this.call('mail_service', 'updateSupportChannelState', value);
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -241,6 +244,21 @@ var SupportChannel = ThreadWithCache.extend({
|
||||
return channelCache.messages;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Posts the message on the Support server.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
* @return {$.Promise}
|
||||
*/
|
||||
_postMessage: function (data) {
|
||||
// ensure that the poll is active before posting the message
|
||||
this.call('mail_service', 'startPollingSupport');
|
||||
return supportSession.rpc('/odoo_im_support/chat_post', {
|
||||
uuid: this._supportChannelUUID,
|
||||
message_content: data.content,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return SupportChannel;
|
||||
|
||||
@@ -315,7 +315,7 @@ QUnit.test('receive messages in the Support channel', function (assert) {
|
||||
body: 'A message',
|
||||
channel_ids: [supportChannelID],
|
||||
};
|
||||
var notification = [[false, 'mail.channel'], data];
|
||||
var notification = [[false, 'mail.channel', 1], data];
|
||||
messagingMenu.call('support_bus_service', 'trigger', 'notification', [notification]);
|
||||
|
||||
assert.strictEqual($('.o_thread_window .o_thread_message').length, 1,
|
||||
|
||||
@@ -117,7 +117,7 @@ var AbstractActivityField = AbstractField.extend({
|
||||
* @param {integer} params.activityID
|
||||
* @param {string} params.feedback
|
||||
*/
|
||||
_markActivityDone: function(params) {
|
||||
_markActivityDone: function (params) {
|
||||
var activityID = params.activityID;
|
||||
var feedback = params.feedback;
|
||||
|
||||
@@ -134,7 +134,7 @@ var AbstractActivityField = AbstractField.extend({
|
||||
* @param {integer} params.activityID
|
||||
* @param {string} params.feedback
|
||||
*/
|
||||
_markActivityDoneAndScheduleNext: function(params) {
|
||||
_markActivityDoneAndScheduleNext: function (params) {
|
||||
var activityID = params.activityID;
|
||||
var feedback = params.feedback;
|
||||
var self = this;
|
||||
@@ -145,8 +145,7 @@ var AbstractActivityField = AbstractField.extend({
|
||||
kwargs: {feedback: feedback},
|
||||
context: this.record.getContext(),
|
||||
}).then(
|
||||
function(rslt_action) {
|
||||
console.log(rslt_action);
|
||||
function (rslt_action) {
|
||||
self.do_action(rslt_action, {
|
||||
on_close: function () {
|
||||
self.trigger_up('reload');
|
||||
@@ -352,7 +351,7 @@ var Activity = AbstractActivityField.extend({
|
||||
force_email: true,
|
||||
},
|
||||
};
|
||||
return this.do_action(action, { on_close: function(){}});
|
||||
return this.do_action(action, { on_close: function () {} });
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -650,7 +649,7 @@ var KanbanActivity = AbstractActivityField.extend({
|
||||
force_email: true,
|
||||
},
|
||||
};
|
||||
return this.do_action(action, { on_close: function(){}});
|
||||
return this.do_action(action, { on_close: function () {} });
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
|
||||
@@ -39,9 +39,9 @@ var Chatter = Widget.extend({
|
||||
* @param {widget} parent
|
||||
* @param {Object} record
|
||||
* @param {Object} mailFields
|
||||
* @param {boolean} [mailFields.mail_activity]
|
||||
* @param {boolean} [mailFields.mail_followers]
|
||||
* @param {boolean} [mailFields.mail_thread]
|
||||
* @param {string} [mailFields.mail_activity]
|
||||
* @param {string} [mailFields.mail_followers]
|
||||
* @param {string} [mailFields.mail_thread]
|
||||
*/
|
||||
init: function (parent, record, mailFields, options) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
@@ -195,6 +195,11 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} [options]
|
||||
// * @param {boolean} [options.phantomjs=false] if set, rendering of non-empty
|
||||
// * thread will use 'block' display instead of 'flex', because phantomjs
|
||||
// * sucks with flexbox... TODO: remove this option when we no longer use
|
||||
// * phantomJS
|
||||
*/
|
||||
init: function (parent, action, options) {
|
||||
this._super.apply(this, arguments);
|
||||
@@ -445,15 +450,12 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
!this._thread.isMassMailing(),
|
||||
displayEmptyThread: !this._thread.hasMessages() && !this.domain.length,
|
||||
displayNoMatchFound: !this._thread.hasMessages() && this.domain.length,
|
||||
displaySubjectOnMessages:
|
||||
(
|
||||
this._thread.getType() !== 'mailbox' &&
|
||||
this._thread.isMassMailing()
|
||||
) ||
|
||||
displaySubjectOnMessages: this._thread.isMassMailing() ||
|
||||
this._thread.getID() === 'mailbox_inbox' ||
|
||||
this._thread.getID() === 'mailbox_moderation',
|
||||
displayEmailIcons: false,
|
||||
displayReplyIcons: true,
|
||||
displayBottomThreadFreeSpace: true,
|
||||
};
|
||||
},
|
||||
/**
|
||||
@@ -495,8 +497,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
(this._threadWidget.el.clientHeight === this._threadWidget.el.scrollHeight) &&
|
||||
!this._thread.isAllHistoryLoaded(this.domain);
|
||||
if (loadMoreMessages) {
|
||||
return this._loadMoreMessages()
|
||||
.then(this._loadEnoughMessages.bind(this));
|
||||
return this._loadMoreMessages().then(this._loadEnoughMessages.bind(this));
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -520,8 +521,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
self._thread,
|
||||
self._getThreadRenderingOptions()
|
||||
);
|
||||
offset += dom.getPosition(document.querySelector(oldestMessageSelector)
|
||||
).top;
|
||||
offset += dom.getPosition(document.querySelector(oldestMessageSelector)).top;
|
||||
self._threadWidget.scrollToPosition(offset);
|
||||
});
|
||||
},
|
||||
@@ -558,7 +558,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
*
|
||||
* @private
|
||||
* @param {JQuery} $input the input to prepare
|
||||
* @param {string} type the type of thread to create ('dm', 'public' or
|
||||
* @param {string} type the type of thread to create ('dm_chat', 'public' or
|
||||
* 'private' channel)
|
||||
*/
|
||||
_prepareAddThreadInput: function ($input, type) {
|
||||
@@ -569,11 +569,11 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
self._lastSearchVal = _.escape(request.term);
|
||||
self._searchChannel(self._lastSearchVal).done(function (result){
|
||||
result.push({
|
||||
'label': _.str.sprintf(
|
||||
label: _.str.sprintf(
|
||||
'<strong>' + _t("Create %s") + '</strong>',
|
||||
'<em>"#' + self._lastSearchVal + '"</em>'
|
||||
),
|
||||
'value': '_create',
|
||||
value: '_create',
|
||||
});
|
||||
response(result);
|
||||
});
|
||||
@@ -599,7 +599,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
self.call('mail_service', 'createChannel', name, 'private');
|
||||
}
|
||||
});
|
||||
} else if (type === 'dm') {
|
||||
} else if (type === 'dm_chat') {
|
||||
$input.autocomplete({
|
||||
source: function (request, response) {
|
||||
self._lastSearchVal = _.escape(request.term);
|
||||
@@ -607,11 +607,11 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
},
|
||||
select: function (ev, ui) {
|
||||
var partnerID = ui.item.id;
|
||||
var dm = self.call('mail_service', 'getDmFromPartnerID', partnerID);
|
||||
if (dm) {
|
||||
self._setThread(dm.getID());
|
||||
var dmChat = self.call('mail_service', 'getDMChatFromPartnerID', partnerID);
|
||||
if (dmChat) {
|
||||
self._setThread(dmChat.getID());
|
||||
} else {
|
||||
self.call('mail_service', 'createChannel', partnerID, 'dm');
|
||||
self.call('mail_service', 'createChannel', partnerID, 'dm_chat');
|
||||
}
|
||||
// clear the input
|
||||
$(this).val('');
|
||||
@@ -693,7 +693,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
needactionCounter: inbox.getMailboxCounter(),
|
||||
starredCounter: starred.getMailboxCounter(),
|
||||
moderationCounter: moderation ? moderation.getMailboxCounter() : 0,
|
||||
isModerator: this.call('mail_service', 'isModerator'),
|
||||
isMyselfModerator: this.call('mail_service', 'isMyselfModerator'),
|
||||
}));
|
||||
return $sidebar;
|
||||
},
|
||||
@@ -919,7 +919,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
*/
|
||||
_unselectMessage: function () {
|
||||
this._basicComposer.do_toggle(this._thread.getType() !== 'mailbox' && !this._thread.isMassMailing());
|
||||
this._extendedComposer.do_toggle(this._thread.getType() !== 'mailbox' && !!this._thread.isMassMailing());
|
||||
this._extendedComposer.do_toggle(this._thread.isMassMailing());
|
||||
|
||||
if (!config.device.isMobile) {
|
||||
var composer = this._thread.getType() !== 'mailbox' && this._thread.isMassMailing() ?
|
||||
@@ -959,7 +959,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
this._thread.getID() === 'mailbox_moderation' ||
|
||||
(
|
||||
this._thread.isModerated() &&
|
||||
this._thread.isModerator()
|
||||
this._thread.isMyselfModerator()
|
||||
)
|
||||
) {
|
||||
this._updateModerationButtons();
|
||||
@@ -985,7 +985,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
* @param {mail.model.Thread} thread
|
||||
*/
|
||||
_updateControlPanelButtons: function (thread) {
|
||||
// Hide 'unsubscribe' button in state channels and DM and channels with group-based subscription
|
||||
// Hide 'unsubscribe' button in state channels and DM chats and channels with group-based subscription
|
||||
this.$buttons
|
||||
.find('.o_mail_discuss_button_invite, .o_mail_discuss_button_settings')
|
||||
.toggle(thread.getType() !== 'dm' && thread.getType() !== 'mailbox');
|
||||
@@ -999,11 +999,11 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
.removeClass('o_hidden');
|
||||
this.$buttons
|
||||
.find('.o_mail_discuss_button_select_all')
|
||||
.toggle((thread.isModerated() && thread.isModerator()) || thread.getID() === 'mailbox_moderation')
|
||||
.toggle((thread.isModerated() && thread.isMyselfModerator()) || thread.getID() === 'mailbox_moderation')
|
||||
.removeClass('o_hidden');
|
||||
this.$buttons
|
||||
.find('.o_mail_discuss_button_unselect_all')
|
||||
.toggle((thread.isModerated() && thread.isModerator()) || thread.getID() === 'mailbox_moderation')
|
||||
.toggle((thread.isModerated() && thread.isMyselfModerator()) || thread.getID() === 'mailbox_moderation')
|
||||
.removeClass('o_hidden');
|
||||
this.$buttons.find('.o_mail_discuss_button_moderate_all').hide();
|
||||
|
||||
@@ -1076,7 +1076,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
});
|
||||
|
||||
this.$('.o_mail_discuss_sidebar').html($sidebar.contents());
|
||||
_.each(['dm', 'public', 'private'], function (type) {
|
||||
_.each(['dm_chat', 'public', 'private'], function (type) {
|
||||
var $input = self.$('.o_mail_add_thread[data-type=' + type + '] input');
|
||||
self._prepareAddThreadInput($input, type);
|
||||
});
|
||||
@@ -1153,16 +1153,16 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
/**
|
||||
* Called when someone asks discuss whether the bottom of `thread` is
|
||||
* visible or not. An object `query` is provided in order to reponse on the
|
||||
* key `isDisplayed`.
|
||||
* key `isVisible`.
|
||||
*
|
||||
* @private
|
||||
* @param {mail.model.Thread} thread
|
||||
* @param {Object} query
|
||||
* @param {boolean} query.isBottomVisible the response to provide on whether
|
||||
* the thread is visible in discuss.
|
||||
* @param {boolean} query.isVisible the response to provide on whether the
|
||||
* bottom of the thread is visible in discuss.
|
||||
*/
|
||||
_onIsThreadBottomVisible: function (thread, query) {
|
||||
query.isBottomVisible = query.isBottomVisible ||
|
||||
query.isVisible = query.isVisible ||
|
||||
(
|
||||
thread.getID() === this._thread.getID() &&
|
||||
this._threadWidget.isAtBottom()
|
||||
@@ -1389,7 +1389,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
|
||||
ev.stopPropagation();
|
||||
var channelID = $(ev.target).data('thread-id');
|
||||
var channel = this.call('mail_service', 'getChannel', channelID);
|
||||
if (channel.isAdministrator()) {
|
||||
if (channel.isMyselfAdministrator()) {
|
||||
this._askConfirmationAdminUnsubscribe(channel);
|
||||
} else {
|
||||
channel.unsubscribe();
|
||||
|
||||
@@ -73,7 +73,7 @@ Discuss.include({
|
||||
_renderButtons: function () {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments);
|
||||
_.each(['dm', 'public', 'private'], function (type) {
|
||||
_.each(['dm_chat', 'public', 'private'], function (type) {
|
||||
var selector = '.o_mail_discuss_button_' + type;
|
||||
self.$buttons.on('click', selector, self._onAddThread.bind(self));
|
||||
});
|
||||
@@ -106,6 +106,7 @@ Discuss.include({
|
||||
*/
|
||||
_setThread: function (threadID) {
|
||||
var thread = this.call('mail_service', 'getThread', threadID);
|
||||
this._thread = thread;
|
||||
if (thread.getType() !== 'mailbox') {
|
||||
this.call('mail_service', 'openThreadWindow', threadID);
|
||||
return $.when();
|
||||
@@ -148,7 +149,7 @@ Discuss.include({
|
||||
*
|
||||
* @private
|
||||
* @param {string} type the thread's type to display (e.g. 'mailbox_inbox',
|
||||
* 'mailbox_starred', 'dm'...).
|
||||
* 'mailbox_starred', 'dm_chat'...).
|
||||
*/
|
||||
_updateContent: function (type) {
|
||||
var self = this;
|
||||
|
||||
@@ -344,7 +344,7 @@ var AbstractMessage = Class.extend({
|
||||
* @return {boolean}
|
||||
*/
|
||||
shouldRedirectToAuthor: function () {
|
||||
return !this._isAuthor();
|
||||
return !this._isMyselfAuthor();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -370,7 +370,7 @@ var AbstractMessage = Class.extend({
|
||||
* @private
|
||||
* @return {boolean}
|
||||
*/
|
||||
_isAuthor: function () {
|
||||
_isMyselfAuthor: function () {
|
||||
return this.hasAuthor() && (this.getAuthorID() === session.partner_id);
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -357,14 +357,6 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin
|
||||
hasTrackingValues: function () {
|
||||
return !!(this._trackingValueIDs && (this._trackingValueIDs.length > 0));
|
||||
},
|
||||
/**
|
||||
* State whether the current user is the author of this message
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
isAuthor: function () {
|
||||
return this._isAuthor();
|
||||
},
|
||||
/**
|
||||
* State whether this message is linked to a document thread (not channel)
|
||||
*
|
||||
@@ -381,6 +373,14 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin
|
||||
isLinkedToDocumentThread: function () {
|
||||
return !!(this._documentModel !== 'mail.channel' && this._documentID);
|
||||
},
|
||||
/**
|
||||
* State whether the current user is the author of this message
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
isMyselfAuthor: function () {
|
||||
return this._isMyselfAuthor();
|
||||
},
|
||||
/**
|
||||
* States whether the current message needs moderation in general.
|
||||
*
|
||||
|
||||
@@ -2,6 +2,7 @@ odoo.define('mail.model.AbstractThread', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Class = require('web.Class');
|
||||
var Mixins = require('web.mixins');
|
||||
|
||||
/**
|
||||
* Abstract thread is the super class of all threads, either backend threads
|
||||
@@ -9,15 +10,20 @@ var Class = require('web.Class');
|
||||
*
|
||||
* Abstract threads contain abstract messages
|
||||
*/
|
||||
var AbstractThread = Class.extend({
|
||||
var AbstractThread = Class.extend(Mixins.EventDispatcherMixin, {
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {Object} params.data
|
||||
* @param {integer|string} params.data.id the ID of this thread
|
||||
* @param {string} params.data.name the name of this thread
|
||||
* @param {string} params.data.status the status of this thread
|
||||
* @param {Object} params.parent Object with the event-dispatcher mixin
|
||||
* (@see {web.mixins.EventDispatcherMixin})
|
||||
*/
|
||||
init: function (params) {
|
||||
Mixins.EventDispatcherMixin.init.call(this, arguments);
|
||||
this.setParent(params.parent);
|
||||
|
||||
this._folded = false; // threads are unfolded by default
|
||||
this._id = params.data.id;
|
||||
this._name = params.data.name;
|
||||
@@ -29,6 +35,15 @@ var AbstractThread = Class.extend({
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Add a message to this thread.
|
||||
*
|
||||
* @param {mail.model.AbstractMessage} message
|
||||
*/
|
||||
addMessage: function (message) {
|
||||
this._addMessage.apply(this, arguments);
|
||||
this.trigger('message_added', message);
|
||||
},
|
||||
/**
|
||||
* Updates the folded state of the thread
|
||||
*
|
||||
@@ -87,12 +102,6 @@ var AbstractThread = Class.extend({
|
||||
hasMessages: function () {
|
||||
return !_.isEmpty(this.getMessages());
|
||||
},
|
||||
/**
|
||||
* Increments the unread counter of this thread by 1 unit.
|
||||
*/
|
||||
incrementUnreadCounter: function () {
|
||||
this._unreadCounter++;
|
||||
},
|
||||
/**
|
||||
* States whether this thread is folded or not.
|
||||
*
|
||||
@@ -101,12 +110,77 @@ var AbstractThread = Class.extend({
|
||||
isFolded: function () {
|
||||
return this._folded;
|
||||
},
|
||||
/**
|
||||
* Mark the thread as read, which resets the unread counter to 0. This is
|
||||
* only performed if the unread counter is not 0.
|
||||
*
|
||||
* @returns {$.Promise}
|
||||
*/
|
||||
markAsRead: function () {
|
||||
if (this._unreadCounter > 0) {
|
||||
return this._markAsRead();
|
||||
}
|
||||
return $.when();
|
||||
},
|
||||
/**
|
||||
* Post a message on this thread
|
||||
*
|
||||
* @returns {$.Promise} resolved with the message object to be sent to the
|
||||
* server
|
||||
*/
|
||||
postMessage: function () {
|
||||
return this._postMessage.apply(this, arguments)
|
||||
.then(this.trigger.bind(this, 'message_posted'));
|
||||
},
|
||||
/**
|
||||
* Resets the unread counter of this thread to 0.
|
||||
*/
|
||||
resetUnreadCounter: function () {
|
||||
this._unreadCounter = 0;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Add a message to this thread.
|
||||
*
|
||||
* @abstract
|
||||
* @private
|
||||
* @param {mail.model.AbstractMessage} message
|
||||
*/
|
||||
_addMessage: function (message) {},
|
||||
/**
|
||||
* Increments the unread counter of this thread by 1 unit.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_incrementUnreadCounter: function () {
|
||||
this._unreadCounter++;
|
||||
},
|
||||
/**
|
||||
* Mark the thread as read
|
||||
*
|
||||
* @private
|
||||
* @returns {$.Promise}
|
||||
*/
|
||||
_markAsRead: function () {
|
||||
this.resetUnreadCounter();
|
||||
this._warnUpdatedUnreadCounter();
|
||||
return $.when();
|
||||
},
|
||||
/**
|
||||
* Post a message on this thread
|
||||
*
|
||||
* @abstract
|
||||
* @private
|
||||
* @returns {$.Promise} resolved with the message object to be sent to the
|
||||
* server
|
||||
*/
|
||||
_postMessage: function () {
|
||||
return $.when();
|
||||
},
|
||||
});
|
||||
|
||||
return AbstractThread;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
odoo.define('mail.model.Channel', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ThreadWithCache = require('mail.model.ThreadWithCache');
|
||||
var SearchableThread = require('mail.model.SearchableThread');
|
||||
var mailUtils = require('mail.utils');
|
||||
|
||||
var session = require('web.session');
|
||||
@@ -15,12 +15,11 @@ var time = require('web.time');
|
||||
* Any piece of code in JS that make use of channels must ideally interact with
|
||||
* such objects, instead of direct data from the server.
|
||||
*/
|
||||
var Channel = ThreadWithCache.extend({
|
||||
var Channel = SearchableThread.extend({
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} params
|
||||
* @param {Object} params.data
|
||||
* @param {string} [params.data.anonymous_name]
|
||||
* @param {string} params.data.channel_type
|
||||
* @param {integer} [params.data.create_uid] the ID of the user that has
|
||||
* created the channel.
|
||||
@@ -29,9 +28,7 @@ var Channel = ThreadWithCache.extend({
|
||||
* @param {boolean} params.data.is_moderator whether the current user is
|
||||
* moderator of this channel.
|
||||
* @param {string} [params.data.last_message_date] date in server-format
|
||||
* @param {boolean} [params.data.mass_mailing]
|
||||
* @param {integer} [params.data.message_unread_counter]
|
||||
* @param {string} [params.data.public] either 'public' or 'private'
|
||||
* @param {string} params.data.state
|
||||
* @param {string} [params.data.uuid]
|
||||
* @param {Object} params.options
|
||||
@@ -48,9 +45,8 @@ var Channel = ThreadWithCache.extend({
|
||||
|
||||
// If set, autoswitch channel on joining this channel in discuss
|
||||
// the default behaviour is to autoswitch on join.
|
||||
// exception: receiving channel or chat session notifications
|
||||
// exception: receiving channel session notifications
|
||||
this._autoswitch = 'autoswitch' in options ? options.autoswitch : true;
|
||||
this._chat = undefined; // FIXME: could be dropped when livechat and DM are moved out of this class
|
||||
this._commands = undefined;
|
||||
this._creatorUID = data.create_uid;
|
||||
this._detached = data.is_minimized;
|
||||
@@ -58,12 +54,12 @@ var Channel = ThreadWithCache.extend({
|
||||
this._folded = data.state === 'folded';
|
||||
// if set: hide 'Leave channel' button
|
||||
this._groupBasedSubscription = data.group_based_subscription;
|
||||
this._isModerator = data.is_moderator;
|
||||
this._isModerated = data.is_moderation;
|
||||
this._isMyselfModerator = data.is_moderator;
|
||||
this._lastMessageDate = undefined;
|
||||
this._massMailing = data.mass_mailing;
|
||||
this._members = [];
|
||||
// Deferred that is resolved on fetched members of this channel.
|
||||
this._membersDef = undefined;
|
||||
this._moderation = data.is_moderation;
|
||||
// number of messages that are 'needaction', which is equivalent to the
|
||||
// number of messages in this channel that are in inbox.
|
||||
this._needactionCounter = data.message_needaction_counter || 0;
|
||||
@@ -78,16 +74,9 @@ var Channel = ThreadWithCache.extend({
|
||||
return !command.channel_types ||
|
||||
_.contains(command.channel_types, self._serverType);
|
||||
});
|
||||
if (this._type === 'channel') {
|
||||
this._type = data.public !== 'private' ? 'public' : 'private';
|
||||
}
|
||||
if ('anonymous_name' in data) {
|
||||
this._name = data.anonymous_name;
|
||||
}
|
||||
if (data.last_message_date) {
|
||||
this._lastMessageDate = moment(time.str_to_datetime(data.last_message_date));
|
||||
}
|
||||
this._chat = !this.getType().match(/^(public|private)$/);
|
||||
if (data.message_unread_counter) {
|
||||
this._unreadCounter = data.message_unread_counter;
|
||||
}
|
||||
@@ -190,6 +179,7 @@ var Channel = ThreadWithCache.extend({
|
||||
* @returns {$.Promise<Object[]>} resolved with list of channel listeners
|
||||
*/
|
||||
getMentionPartnerSuggestions: function () {
|
||||
var self = this;
|
||||
if (!this._membersDef) {
|
||||
this._membersDef = this._rpc({
|
||||
model: 'mail.channel',
|
||||
@@ -199,6 +189,7 @@ var Channel = ThreadWithCache.extend({
|
||||
shadow: true
|
||||
})
|
||||
.then(function (members) {
|
||||
self._members = members;
|
||||
return members;
|
||||
});
|
||||
}
|
||||
@@ -215,7 +206,7 @@ var Channel = ThreadWithCache.extend({
|
||||
*/
|
||||
getPreview: function () {
|
||||
var result = this._super.apply(this, arguments);
|
||||
if (!this.isChat()) {
|
||||
if (!this.isTwoUserThread()) {
|
||||
result.imageSRC = '/web/image/mail.channel/' + this.getID() + '/image_small';
|
||||
}
|
||||
var lastMessage = this.getLastMessage();
|
||||
@@ -223,7 +214,7 @@ var Channel = ThreadWithCache.extend({
|
||||
author: lastMessage ? lastMessage.getDisplayedAuthor() : '',
|
||||
body: lastMessage ? mailUtils.parseAndTransform(lastMessage.getBody(), mailUtils.inline) : '',
|
||||
date: lastMessage ? lastMessage.getDate() : moment(),
|
||||
isAuthor: this.hasMessages() && this.getLastMessage().isAuthor(),
|
||||
isMyselfAuthor: this.hasMessages() && this.getLastMessage().isMyselfAuthor(),
|
||||
});
|
||||
},
|
||||
/**
|
||||
@@ -251,15 +242,6 @@ var Channel = ThreadWithCache.extend({
|
||||
incrementNeedactionCounter: function () {
|
||||
this._needactionCounter++;
|
||||
},
|
||||
/**
|
||||
* Tells whether the current user is administrator of the channel.
|
||||
* Note that there is no administrator for chat channels
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isAdministrator: function () {
|
||||
return session.uid === this._creatorUID && !this.isChat();
|
||||
},
|
||||
/**
|
||||
* States whether the channel should be auto-selected on creation
|
||||
*
|
||||
@@ -281,18 +263,6 @@ var Channel = ThreadWithCache.extend({
|
||||
isChannel: function () {
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* States whether this channel is a chat or not.
|
||||
* These types of channels are chat:
|
||||
* - direct messages (DM)
|
||||
* - livechat
|
||||
*
|
||||
* @override
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isChat: function () {
|
||||
return this._chat;
|
||||
},
|
||||
/**
|
||||
* States whether the channel auto-subscribes some users in a group
|
||||
*
|
||||
@@ -308,81 +278,32 @@ var Channel = ThreadWithCache.extend({
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isModerated: function () {
|
||||
return this._moderation;
|
||||
return this._isModerated;
|
||||
},
|
||||
/**
|
||||
* Tells whether the current user is administrator of the channel.
|
||||
* Note that there is no administrator for two-user channels
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isMyselfAdministrator: function () {
|
||||
return session.uid === this._creatorUID && !this.isTwoUserThread();
|
||||
},
|
||||
/**
|
||||
* States whether the current user is moderator of this channel.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isModerator: function () {
|
||||
return this._isModerator;
|
||||
},
|
||||
/**
|
||||
* Marks this channel as read.
|
||||
* The last seen message will be the last message.
|
||||
* Resolved with the last seen message, only for non-mailbox channels
|
||||
*
|
||||
* @override
|
||||
* @returns {$.Promise<integer|undefined>} resolved with last message ID
|
||||
* seen in the channel, and when the channel has been marked as seen on
|
||||
* the server.
|
||||
*/
|
||||
markAsRead: function () {
|
||||
if (this._unreadCounter > 0) {
|
||||
this.resetUnreadCounter();
|
||||
return this._throttleFetchSeen();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* Prepare and send a message to the server on this channel
|
||||
*
|
||||
* @override
|
||||
* @param {Object} data data related to the new message
|
||||
* @returns {$.Promise<Object>} resolved when the message has been sent to
|
||||
* the server, with the object message that has been sent to the server.
|
||||
*/
|
||||
postMessage: function (data) {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function (messageData) {
|
||||
_.extend(messageData, {
|
||||
message_type: 'comment',
|
||||
subtype: 'mail.mt_comment',
|
||||
command: data.command,
|
||||
});
|
||||
return self._rpc({
|
||||
model: 'mail.channel',
|
||||
method: data.command ? 'execute_command' : 'message_post',
|
||||
args: [self._id],
|
||||
kwargs: messageData,
|
||||
}).then(function () {
|
||||
return messageData;
|
||||
});
|
||||
});
|
||||
isMyselfModerator: function () {
|
||||
return this._isMyselfModerator;
|
||||
},
|
||||
/**
|
||||
* Unsubscribes from channel
|
||||
*
|
||||
* @abstract
|
||||
* @returns {$.Promise} resolve when unsubscribed
|
||||
*/
|
||||
unsubscribe: function () {
|
||||
if (_.contains(['public', 'private'], this.getType())) {
|
||||
// unfollow channel
|
||||
return this._rpc({
|
||||
model: 'mail.channel',
|
||||
method: 'action_unfollow',
|
||||
args: [[this._id]],
|
||||
});
|
||||
} else {
|
||||
// unpin livechat
|
||||
return this._rpc({
|
||||
model: 'mail.channel',
|
||||
method: 'channel_pin',
|
||||
args: [this.getUUID(), false],
|
||||
});
|
||||
}
|
||||
},
|
||||
unsubscribe: function () {},
|
||||
/**
|
||||
* Updates the internal state of the channel, and reflects the changes in
|
||||
* the UI.
|
||||
@@ -453,6 +374,47 @@ var Channel = ThreadWithCache.extend({
|
||||
['need_moderation', '=', true],
|
||||
['channel_ids', 'in', [this._id]]];
|
||||
},
|
||||
/**
|
||||
* Marks this channel as read.
|
||||
* The last seen message will be the last message.
|
||||
* Resolved with the last seen message, only for non-mailbox channels
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
* @returns {$.Promise} resolved when message has been marked as read
|
||||
*/
|
||||
_markAsRead: function () {
|
||||
var superDef = this._super.apply(this, arguments);
|
||||
var seenDef = this._throttleFetchSeen();
|
||||
return $.when(superDef, seenDef);
|
||||
},
|
||||
/**
|
||||
* Prepare and send a message to the server on this channel.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
* @param {Object} data data related to the new message
|
||||
* @returns {$.Promise<Object>} resolved when the message has been sent to
|
||||
* the server, with the object message that has been sent to the server.
|
||||
*/
|
||||
_postMessage: function (data) {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function (messageData) {
|
||||
_.extend(messageData, {
|
||||
message_type: 'comment',
|
||||
subtype: 'mail.mt_comment',
|
||||
command: data.command,
|
||||
});
|
||||
return self._rpc({
|
||||
model: 'mail.channel',
|
||||
method: data.command ? 'execute_command' : 'message_post',
|
||||
args: [self._id],
|
||||
kwargs: messageData,
|
||||
}).then(function () {
|
||||
return messageData;
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return Channel;
|
||||
|
||||
+9
-32
@@ -1,19 +1,18 @@
|
||||
odoo.define('mail.model.DM', function (require) {
|
||||
odoo.define('mail.model.DMChat', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Channel = require('mail.model.Channel');
|
||||
var TwoUserChannel = require('mail.model.TwoUserChannel');
|
||||
|
||||
/**
|
||||
*
|
||||
* Any piece of code in JS that make use of DMs must ideally interact with
|
||||
* such objects, instead of direct data from the server.
|
||||
*/
|
||||
var DM = Channel.extend({
|
||||
var DMChat = TwoUserChannel.extend({
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} params
|
||||
* @param {Object} params.data
|
||||
* @param {Object} params.data.direct_partner
|
||||
* @param {Object[]} params.data.direct_partner
|
||||
* @param {integer} params.data.direct_partner[0].id
|
||||
* @param {string} params.data.direct_partner[0].im_status
|
||||
* @param {string} params.data.direct_partner[0].name
|
||||
@@ -23,10 +22,10 @@ var DM = Channel.extend({
|
||||
|
||||
var data = params.data;
|
||||
|
||||
this._type = 'dm';
|
||||
this._name = data.direct_partner[0].name;
|
||||
this._directPartnerID = data.direct_partner[0].id;
|
||||
this._name = data.direct_partner[0].name;
|
||||
this._status = data.direct_partner[0].im_status;
|
||||
this._type = 'dm_chat';
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -35,8 +34,8 @@ var DM = Channel.extend({
|
||||
|
||||
/**
|
||||
* Get the direct partner ID linked to the DM, i.e. the partner ID of the
|
||||
* user at the other end of the DM conversation. All DM do have a direct
|
||||
* partner iD.
|
||||
* user at the other end of the DM conversation. All DM chats do have a
|
||||
* direct partner iD.
|
||||
*
|
||||
* @returns {integer}
|
||||
*/
|
||||
@@ -51,36 +50,14 @@ var DM = Channel.extend({
|
||||
result.imageSRC = '/web/image/res.partner/' + this.getDirectPartnerID() + '/image_small';
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* DM are chat
|
||||
*
|
||||
* @override
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isChat: function () {
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* @param {string} newStatus
|
||||
*/
|
||||
setStatus: function (newStatus) {
|
||||
this._status = newStatus;
|
||||
},
|
||||
/**
|
||||
* Unpin this DM
|
||||
*
|
||||
* @override
|
||||
* @return {$.Promise} resolve when unpinned
|
||||
*/
|
||||
unsubscribe: function () {
|
||||
return this._rpc({
|
||||
model: 'mail.channel',
|
||||
method: 'channel_pin',
|
||||
args: [this.getUUID(), false],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return DM;
|
||||
return DMChat;
|
||||
|
||||
});
|
||||
@@ -57,28 +57,6 @@ var DocumentThread = Thread.extend({
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Add this message to this document thread.
|
||||
*
|
||||
* This is ignored if the message is already linked to this document thread.
|
||||
*
|
||||
* @override
|
||||
* @param {mail.model.Message} message
|
||||
*/
|
||||
addMessage: function (message) {
|
||||
if (_.contains(this._messages, message)) {
|
||||
return;
|
||||
}
|
||||
// update internal list of messages
|
||||
this._messages.push(message);
|
||||
this._messages = _.sortBy(this._messages, function (msg) {
|
||||
return msg.getID();
|
||||
});
|
||||
// update message ids associated to this document thread
|
||||
if (!_.contains(this._messageIDs, message.getID())) {
|
||||
this._messageIDs.push(message.getID());
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Overrides to store the thread's state in the LocalStorage, so that it is
|
||||
* shared between tabs, and restored on F5.
|
||||
@@ -178,59 +156,6 @@ var DocumentThread = Thread.extend({
|
||||
isLinkedToDocument: function () {
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* Overrides this method so that all the messages of this document thread
|
||||
* are marked as read on the server.
|
||||
*
|
||||
* @override
|
||||
* @returns {$.Promise} resolved when messages have been marked as read on
|
||||
* the server.
|
||||
*/
|
||||
markAsRead: function () {
|
||||
this._super.apply(this, arguments);
|
||||
return this.call('mail_service', 'markMessagesAsRead', this._messageIDs);
|
||||
},
|
||||
/**
|
||||
* Post message for document thread
|
||||
*
|
||||
* @override
|
||||
* @param {Object} data data related to the new message
|
||||
* @return {$.Promise<Object>} resolved when the message has been sent to
|
||||
* the server, with the object message that has been sent to the server.
|
||||
*/
|
||||
postMessage: function (data) {
|
||||
var self = this;
|
||||
var resModel = this.getDocumentModel();
|
||||
var resID = this.getDocumentID();
|
||||
return this._super.apply(this, arguments)
|
||||
.then(function (messageData) {
|
||||
_.extend(messageData, {
|
||||
context: data.context,
|
||||
message_type: data.message_type,
|
||||
subtype: data.subtype || "mail.mt_comment",
|
||||
subtype_id: data.subtype_id,
|
||||
});
|
||||
return self._rpc({
|
||||
model: resModel,
|
||||
method: 'message_post',
|
||||
args: [resID],
|
||||
kwargs: messageData,
|
||||
})
|
||||
.then(function (messageID) {
|
||||
return self._rpc({
|
||||
model: 'mail.message',
|
||||
method: 'message_format',
|
||||
args: [[messageID]],
|
||||
})
|
||||
.then(function (messages) {
|
||||
messages[0].model = resModel;
|
||||
messages[0].res_id = resID;
|
||||
self.call('mail_service', 'addMessage', messages[0]);
|
||||
return messages[0];
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Set list of message IDs of this document thread
|
||||
*
|
||||
@@ -259,6 +184,30 @@ var DocumentThread = Thread.extend({
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Add this message to this document thread.
|
||||
*
|
||||
* This is ignored if the message is already linked to this document thread.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
* @param {mail.model.Message} message
|
||||
*/
|
||||
_addMessage: function (message) {
|
||||
this._super.apply(this, arguments);
|
||||
if (_.contains(this._messages, message)) {
|
||||
return;
|
||||
}
|
||||
// update internal list of messages
|
||||
this._messages.push(message);
|
||||
this._messages = _.sortBy(this._messages, function (msg) {
|
||||
return msg.getID();
|
||||
});
|
||||
// update message ids associated to this document thread
|
||||
if (!_.contains(this._messageIDs, message.getID())) {
|
||||
this._messageIDs.push(message.getID());
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Get most up to date messageIDs
|
||||
*
|
||||
@@ -324,6 +273,63 @@ var DocumentThread = Thread.extend({
|
||||
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Overrides this method so that all the messages of this document thread
|
||||
* are marked as read on the server.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
* @returns {$.Promise} resolved when messages have been marked as read on
|
||||
* the server.
|
||||
*/
|
||||
_markAsRead: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self.call('mail_service', 'markMessagesAsRead', self._messageIDs);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Post message for document thread
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
* @param {Object} data data related to the new message
|
||||
* @return {$.Promise<Object>} resolved when the message has been sent to
|
||||
* the server, with the object message that has been sent to the server.
|
||||
*/
|
||||
_postMessage: function (data) {
|
||||
var self = this;
|
||||
var resModel = this.getDocumentModel();
|
||||
var resID = this.getDocumentID();
|
||||
return this._super.apply(this, arguments)
|
||||
.then(function (messageData) {
|
||||
_.extend(messageData, {
|
||||
context: data.context,
|
||||
message_type: data.message_type,
|
||||
subtype: data.subtype || "mail.mt_comment",
|
||||
subtype_id: data.subtype_id,
|
||||
});
|
||||
return self._rpc({
|
||||
model: resModel,
|
||||
method: 'message_post',
|
||||
args: [resID],
|
||||
kwargs: messageData,
|
||||
})
|
||||
.then(function (messageID) {
|
||||
return self._rpc({
|
||||
model: 'mail.message',
|
||||
method: 'message_format',
|
||||
args: [[messageID]],
|
||||
})
|
||||
.then(function (messages) {
|
||||
messages[0].model = resModel;
|
||||
messages[0].res_id = resID;
|
||||
self.call('mail_service', 'addMessage', messages[0]);
|
||||
return messages[0];
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return DocumentThread;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
odoo.define('mail.model.Livechat', function (require) {
|
||||
"use strict";
|
||||
|
||||
var TwoUserChannel = require('mail.model.TwoUserChannel');
|
||||
|
||||
/**
|
||||
* backend-side of the livechat.
|
||||
*
|
||||
* Any piece of code in JS that make use of Livechats must ideally interact with
|
||||
* such objects, instead of direct data from the server.
|
||||
*/
|
||||
var Livechat = TwoUserChannel.extend({
|
||||
_WEBSITE_USER_ID: '_websiteUser',
|
||||
_WEBSITE_USER_NAME: 'Website user',
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} params
|
||||
* @param {Object} params.data
|
||||
* @param {string} params.data.anonymous_name name of the website user
|
||||
*/
|
||||
init: function (params) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this._name = params.data.anonymous_name;
|
||||
|
||||
this._WEBSITE_USER_NAME = this._name;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* For the livechat,
|
||||
*
|
||||
* @override
|
||||
* @returns {$.Promise<Object[]>} resolved with list of livechat members
|
||||
*/
|
||||
getMentionPartnerSuggestions: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
var websiteUser = _.findWhere(self._members, { id: self._WEBSITE_USER_ID });
|
||||
if (!websiteUser) {
|
||||
self._members.push({
|
||||
id: self._WEBSITE_USER_ID,
|
||||
name: self._WEBSITE_USER_NAME,
|
||||
});
|
||||
}
|
||||
return self._members;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return Livechat;
|
||||
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
odoo.define('mail.model.Mailbox', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ThreadWithCache = require('mail.model.ThreadWithCache');
|
||||
var SearchableThread = require('mail.model.SearchableThread');
|
||||
|
||||
var core = require('web.core');
|
||||
|
||||
@@ -14,7 +14,7 @@ var _t = core._t;
|
||||
* list of messages, but the inbox does not represent a conversation: Inbox is
|
||||
* modeled as a mailbox.
|
||||
*/
|
||||
var Mailbox = ThreadWithCache.extend({
|
||||
var Mailbox = SearchableThread.extend({
|
||||
|
||||
/**
|
||||
* @override
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
odoo.define('mail.model.MultiUserChannel', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Channel = require('mail.model.Channel');
|
||||
|
||||
var MultiUserChannel = Channel.extend({
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} params
|
||||
* @param {Object} params.data
|
||||
* @param {boolean} [params.data.mass_mailing=false]
|
||||
* @param {string} [params.data.public='public'] either 'public' or
|
||||
* 'private'
|
||||
*/
|
||||
init: function (params) {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
var data = params.data;
|
||||
|
||||
this._isMassMailing = data.mass_mailing || false;
|
||||
this._type = data.public !== 'private' ? 'public' : 'private';
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* States whether this thread has the mass mailing setting active or not.
|
||||
* This is a server-side setting, that determine the type of composer that
|
||||
* is used (basic or extended composer).
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
isMassMailing: function () {
|
||||
return this._isMassMailing;
|
||||
},
|
||||
/**
|
||||
* Unsubscribes from channel
|
||||
*
|
||||
* @override
|
||||
* @returns {$.Promise} resolve when unsubscribed
|
||||
*/
|
||||
unsubscribe: function () {
|
||||
return this._rpc({
|
||||
model: 'mail.channel',
|
||||
method: 'action_unfollow',
|
||||
args: [[this.getID()]],
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
return MultiUserChannel;
|
||||
|
||||
});
|
||||
+21
-20
@@ -1,4 +1,4 @@
|
||||
odoo.define('mail.model.ThreadWithCache', function (require) {
|
||||
odoo.define('mail.model.SearchableThread', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Thread = require('mail.model.Thread');
|
||||
@@ -13,7 +13,7 @@ var session = require('web.session');
|
||||
* Any threads that are instances of this Class can be used with a search view,
|
||||
* in order to make searches on messages in a thread.
|
||||
*/
|
||||
var ThreadWithCache = Thread.extend({
|
||||
var SearchableThread = Thread.extend({
|
||||
|
||||
/**
|
||||
* @override
|
||||
@@ -38,23 +38,6 @@ var ThreadWithCache = Thread.extend({
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Add the message to this thread
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
* @param {mail.model.Message} message
|
||||
* @param {Array} domain
|
||||
*/
|
||||
addMessage: function (message, domain) {
|
||||
var cache = this._getCache(domain);
|
||||
var index = _.sortedIndex(cache.messages, message, function (msg) {
|
||||
return msg.getID();
|
||||
});
|
||||
if (cache.messages[index] !== message) {
|
||||
cache.messages.splice(index, 0, message);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @param {Array} [domain]
|
||||
@@ -128,6 +111,24 @@ var ThreadWithCache = Thread.extend({
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Add the message to this thread
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
* @param {mail.model.Message} message
|
||||
* @param {Array} domain
|
||||
*/
|
||||
_addMessage: function (message, domain) {
|
||||
this._super.apply(this, arguments);
|
||||
var cache = this._getCache(domain);
|
||||
var index = _.sortedIndex(cache.messages, message, function (msg) {
|
||||
return msg.getID();
|
||||
});
|
||||
if (cache.messages[index] !== message) {
|
||||
cache.messages.splice(index, 0, message);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Gets messages from thread
|
||||
*
|
||||
@@ -202,6 +203,6 @@ var ThreadWithCache = Thread.extend({
|
||||
_getThreadDomain: function () {},
|
||||
});
|
||||
|
||||
return ThreadWithCache;
|
||||
return SearchableThread;
|
||||
|
||||
});
|
||||
@@ -5,7 +5,6 @@ var emojis = require('mail.emojis');
|
||||
var AbstractThread = require('mail.model.AbstractThread');
|
||||
var mailUtils = require('mail.utils');
|
||||
|
||||
var Mixins = require('web.mixins');
|
||||
var ServicesMixin = require('web.ServicesMixin');
|
||||
|
||||
/**
|
||||
@@ -15,7 +14,7 @@ var ServicesMixin = require('web.ServicesMixin');
|
||||
*
|
||||
* In particular, channels and mailboxes are two different kinds of threads.
|
||||
*/
|
||||
var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
var Thread = AbstractThread.extend(ServicesMixin, {
|
||||
|
||||
/**
|
||||
* @override
|
||||
@@ -27,15 +26,9 @@ var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
* @param {string} [params.data.type]
|
||||
*/
|
||||
init: function (params) {
|
||||
Mixins.EventDispatcherMixin.init.call(this, arguments);
|
||||
this.setParent(params.parent);
|
||||
this._super.apply(this, arguments);
|
||||
// threads are not detached by default
|
||||
this._detached = false;
|
||||
// if this._massMailing is set, display subject on messages, use
|
||||
// extended composer and show "Send by messages by email" on discuss
|
||||
// sidebar
|
||||
this._massMailing = false;
|
||||
// on 1st request to getPreview, fetch data if incomplete. Otherwise it
|
||||
// means that there is no message in this channel.
|
||||
this._previewed = false;
|
||||
@@ -48,13 +41,6 @@ var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Add a message to this thread.
|
||||
*
|
||||
* @abstract
|
||||
* @param {mail.model.Message} message
|
||||
*/
|
||||
addMessage: function (message) {},
|
||||
/**
|
||||
* Updates the _detached state of the thread. Must be overriden to reflect
|
||||
* the new state in the interface.
|
||||
@@ -140,7 +126,7 @@ var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
return {
|
||||
id: this.getID(),
|
||||
imageSRC: '/mail/static/src/img/smiley/avatar.jpg',
|
||||
isChat: this.isChat(),
|
||||
isTwoUserThread: this.isTwoUserThread(),
|
||||
status: this.getStatus(),
|
||||
title: this.getName(),
|
||||
unreadCounter: this.getUnreadCounter(),
|
||||
@@ -188,16 +174,6 @@ var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
* @abstract
|
||||
*/
|
||||
incrementNeedactionCounter: function () {},
|
||||
/**
|
||||
* Increment the unread counter of this thread by 1 unit, and warn that the
|
||||
* counter has been changed.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
incrementUnreadCounter: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this._warnUpdatedUnreadCounter();
|
||||
},
|
||||
/**
|
||||
* States whether the thread should be auto-selected on creation
|
||||
*
|
||||
@@ -222,17 +198,6 @@ var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
isChannel: function () {
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* States whether this thread is a chat or not. In particular, public and
|
||||
* private channels are not chat, but DMs and Livechats are chats. Chats
|
||||
* are threads used for communication between two users.
|
||||
* By default, any thread is not a chat
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isChat: function () {
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* States whether this thread is detached or not.
|
||||
* A thread that is detached must have a thread window linked to itself.
|
||||
@@ -256,10 +221,12 @@ var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
* This is a server-side setting, that determine the type of composer that
|
||||
* is used (basic or extended composer).
|
||||
*
|
||||
* By default, thread have not mass-mailings feature set.
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
isMassMailing: function () {
|
||||
return this._massMailing;
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -277,7 +244,18 @@ var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isModerator: function () {
|
||||
isMyselfModerator: function () {
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* States whether this is a two-user thread or not. In particular, public
|
||||
* and private channels are not two-user threads, but DMs and Livechats are
|
||||
* two-user threads. Two-user threads are used for communication between
|
||||
* two users. By default, any thread is not two-user thread.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isTwoUserThread: function () {
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
@@ -294,59 +272,6 @@ var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
markAsPreviewed: function () {
|
||||
this._previewed = true;
|
||||
},
|
||||
/**
|
||||
* Mark the thread as read, which resets the unread counter to 0.
|
||||
*
|
||||
* @returns {$.Promise} resolved
|
||||
*/
|
||||
markAsRead: function () {
|
||||
if (this._unreadCounter > 0) {
|
||||
this.resetUnreadCounter();
|
||||
}
|
||||
return $.when();
|
||||
},
|
||||
/**
|
||||
* Post a message on the thread.
|
||||
*
|
||||
* This method must be completed by concrete threads,
|
||||
* As it currently only pre-process the messages at the moment.
|
||||
*
|
||||
* @abstract
|
||||
* @param {Object} data
|
||||
* @returns {$.Promise<Object>} resolved with the message object to be sent
|
||||
* to the server
|
||||
*/
|
||||
postMessage: function (data) {
|
||||
// This message will be received from the mail composer as html content
|
||||
// subtype but the urls will not be linkified. If the mail composer
|
||||
// takes the responsibility to linkify the urls we end up with double
|
||||
// linkification a bit everywhere. Ideally we want to keep the content
|
||||
// as text internally and only make html enrichment at display time but
|
||||
// the current design makes this quite hard to do.
|
||||
var body = mailUtils.parseAndTransform(_.str.trim(data.content), mailUtils.addLink);
|
||||
body = this._generateEmojis(body);
|
||||
var messageData = {
|
||||
partner_ids: data.partner_ids,
|
||||
body: body,
|
||||
attachment_ids: data.attachment_ids,
|
||||
canned_response_ids: data.canned_response_ids,
|
||||
};
|
||||
if ('subject' in data) {
|
||||
messageData.subject = data.subject;
|
||||
}
|
||||
return $.when(messageData);
|
||||
},
|
||||
/**
|
||||
* Overrides the method so that it also warns that the counter has been
|
||||
* changed on this thread.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
resetUnreadCounter: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this._warnUpdatedUnreadCounter();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
@@ -379,13 +304,58 @@ var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
return htmlString;
|
||||
},
|
||||
/**
|
||||
* Warn on the chat bus that the unread counter has been updated
|
||||
* Increment the unread counter of this thread by 1 unit, and warn that the
|
||||
* counter has been changed.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_incrementUnreadCounter: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this._warnUpdatedUnreadCounter();
|
||||
},
|
||||
/**
|
||||
* Post a message on the thread.
|
||||
*
|
||||
* This method must be completed by concrete threads,
|
||||
* As it currently only pre-process the messages at the moment.
|
||||
*
|
||||
* @abstract
|
||||
* @private
|
||||
* @param {Object} data
|
||||
* @returns {$.Promise<Object>} resolved with the message object to be sent
|
||||
* to the server
|
||||
*/
|
||||
_postMessage: function (data) {
|
||||
// This message will be received from the mail composer as html content
|
||||
// subtype but the urls will not be linkified. If the mail composer
|
||||
// takes the responsibility to linkify the urls we end up with double
|
||||
// linkification a bit everywhere. Ideally we want to keep the content
|
||||
// as text internally and only make html enrichment at display time but
|
||||
// the current design makes this quite hard to do.
|
||||
var body = mailUtils.parseAndTransform(_.str.trim(data.content), mailUtils.addLink);
|
||||
body = this._generateEmojis(body);
|
||||
var messageData = {
|
||||
partner_ids: data.partner_ids,
|
||||
body: body,
|
||||
attachment_ids: data.attachment_ids,
|
||||
canned_response_ids: data.canned_response_ids,
|
||||
};
|
||||
if ('subject' in data) {
|
||||
messageData.subject = data.subject;
|
||||
}
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
return $.when(messageData);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Warn on the mail bus that the unread counter has been updated
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_warnUpdatedUnreadCounter: function () {
|
||||
this.call('mail_service', 'getMailBus')
|
||||
.trigger('update_thread_unread_counter', this);
|
||||
this.call('mail_service', 'getMailBus').trigger('update_thread_unread_counter', this);
|
||||
},
|
||||
/**
|
||||
* Warn other mail components that the window state of this thread has
|
||||
@@ -393,6 +363,7 @@ var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
*
|
||||
* @private
|
||||
* @param {Object} [options={}]
|
||||
* @param {boolean} [options.passively=false]
|
||||
*/
|
||||
_warnUpdatedWindowState: function (options) {
|
||||
options = options || {};
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
odoo.define('mail.model.TwoUserChannel', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Channel = require('mail.model.Channel');
|
||||
|
||||
var TwoUserChannel = Channel.extend({
|
||||
/**
|
||||
* @override
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isTwoUserThread: function () {
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* Unpin from two-user thread
|
||||
*
|
||||
* @override
|
||||
* @returns {$.Promise} resolve when unsubscribed
|
||||
*/
|
||||
unsubscribe: function () {
|
||||
return this._rpc({
|
||||
model: 'mail.channel',
|
||||
method: 'channel_pin',
|
||||
args: [this.getUUID(), false],
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
return TwoUserChannel;
|
||||
|
||||
});
|
||||
@@ -26,11 +26,12 @@ odoo.define('mail.Manager', function (require) {
|
||||
*/
|
||||
var AbstractService = require('web.AbstractService');
|
||||
|
||||
var Channel = require('mail.model.Channel');
|
||||
var DM = require('mail.model.DM');
|
||||
var DMChat = require('mail.model.DMChat');
|
||||
var Livechat = require('mail.model.Livechat');
|
||||
var Mailbox = require('mail.model.Mailbox');
|
||||
var MailFailure = require('mail.model.MailFailure');
|
||||
var Message = require('mail.model.Message');
|
||||
var MultiUserChannel = require('mail.model.MultiUserChannel');
|
||||
var mailUtils = require('mail.utils');
|
||||
|
||||
var Bus = require('web.Bus');
|
||||
@@ -39,7 +40,6 @@ var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
|
||||
var _t = core._t;
|
||||
var _lt = core._lt;
|
||||
|
||||
var PREVIEW_MSG_MAX_SIZE = 350; // optimal for native english speakers
|
||||
|
||||
@@ -52,30 +52,10 @@ var MailManager = AbstractService.extend({
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this._cannedResponses = [];
|
||||
this._mailBus = new Bus(this);
|
||||
this._commands = [];
|
||||
this._discussMenuID = undefined;
|
||||
this._discussOpen = false;
|
||||
this._isModerator = false;
|
||||
this._mailFailures = [];
|
||||
// list of employees for chatter mentions
|
||||
this._mentionPartnerSuggestions = [];
|
||||
this._messages = [];
|
||||
this._moderatedChannelIDs = [];
|
||||
// # of message received when odoo is out of focus
|
||||
this._outOfFocusUnreadMessageCounter = 0;
|
||||
// partner_ids we have a pinned DM with
|
||||
this._pinnedDmPartners = [];
|
||||
// all threads, including channels, DM, mailboxes, document threads, ...
|
||||
this._threads = [];
|
||||
|
||||
// listen on buses
|
||||
this._mailBus
|
||||
.on('discuss_open', this, this._onDiscussOpen)
|
||||
.on('window_focus', this, this._onWindowFocus);
|
||||
|
||||
this._initializeFromServer();
|
||||
this._super.apply(this, arguments);
|
||||
this._initializeInternalState();
|
||||
this._listenOnBuses();
|
||||
this._fetchMailStateFromServer();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -100,17 +80,7 @@ var MailManager = AbstractService.extend({
|
||||
options = options || {};
|
||||
var message = this.getMessage(data.id);
|
||||
if (!message) {
|
||||
message = this._makeMessage(data);
|
||||
// Keep the array ordered by ID when inserting the new message
|
||||
var index = _.sortedIndex(this._messages, message, function (msg) {
|
||||
return msg.getID();
|
||||
});
|
||||
this._messages.splice(index, 0, message);
|
||||
this._addNewMessagePostprocessThread(message, options);
|
||||
this._addMessageToThreads(message, []);
|
||||
if (!options.silent) {
|
||||
this._mailBus.trigger('new_message', message);
|
||||
}
|
||||
message = this._addNewMessage(data, options);
|
||||
} else {
|
||||
if (data.moderation_status === 'accepted') {
|
||||
message.setModerationStatus('accepted', {
|
||||
@@ -124,16 +94,17 @@ var MailManager = AbstractService.extend({
|
||||
return message;
|
||||
},
|
||||
/**
|
||||
* Creates a channel, can be either a true channel or a DM based on `type`
|
||||
* Creates a channel, can be either a true channel or a DM chat based on
|
||||
* `type`
|
||||
*
|
||||
* @param {integer|string} name id of partner (in case of dm) or name
|
||||
* @param {string} type ['dm', 'public', 'private']
|
||||
* @param {string} type ['dm_chat', 'public', 'private']
|
||||
* @returns {$.Promise<integer>} resolved with ID of the newly created
|
||||
* channel
|
||||
*/
|
||||
createChannel: function (name, type) {
|
||||
if (type === 'dm') {
|
||||
return this._createDM(name);
|
||||
if (type === 'dm_chat') {
|
||||
return this._createDMChat(name);
|
||||
} else {
|
||||
return this._createChannel(name, type);
|
||||
}
|
||||
@@ -202,9 +173,9 @@ var MailManager = AbstractService.extend({
|
||||
* @param {integer} partnerID
|
||||
* @returns {Object|undefined} channel
|
||||
*/
|
||||
getDmFromPartnerID: function (partnerID) {
|
||||
getDMChatFromPartnerID: function (partnerID) {
|
||||
return _.find(this._threads, function (thread) {
|
||||
return thread.getType() === 'dm' &&
|
||||
return thread.getType() === 'dm_chat' &&
|
||||
thread.getDirectPartnerID() === partnerID;
|
||||
});
|
||||
},
|
||||
@@ -314,8 +285,8 @@ var MailManager = AbstractService.extend({
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isModerator: function () {
|
||||
return this._isModerator;
|
||||
isMyselfModerator: function () {
|
||||
return this._isMyselfModerator;
|
||||
},
|
||||
/**
|
||||
* States whether the mail manager is ready or not
|
||||
@@ -381,7 +352,7 @@ var MailManager = AbstractService.extend({
|
||||
* Special redirection handling for given model and id
|
||||
*
|
||||
* If the model is res.partner, and there is a user associated with this
|
||||
* partner which isn't the current user, open the DM with this user.
|
||||
* partner which isn't the current user, open the DM chat with this user.
|
||||
* Otherwhise, open the record's form view (if not current user's).
|
||||
*
|
||||
* @param {string} resModel model to open
|
||||
@@ -530,6 +501,28 @@ var MailManager = AbstractService.extend({
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Add a new message
|
||||
*
|
||||
* @private
|
||||
* @param {Object} data
|
||||
* @param {Object} options
|
||||
* @returns {mail.model.Message}
|
||||
*/
|
||||
_addNewMessage: function (data, options) {
|
||||
var message = this._makeMessage(data);
|
||||
// Keep the array ordered by ID when inserting the new message
|
||||
var index = _.sortedIndex(this._messages, message, function (msg) {
|
||||
return msg.getID();
|
||||
});
|
||||
this._messages.splice(index, 0, message);
|
||||
this._addNewMessagePostprocessThread(message, options);
|
||||
this._addMessageToThreads(message, []);
|
||||
if (!options.silent) {
|
||||
this._mailBus.trigger('new_message', message);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
/**
|
||||
* For newly added message, postprocess threads linked to this message
|
||||
*
|
||||
@@ -547,13 +540,14 @@ var MailManager = AbstractService.extend({
|
||||
if (thread) {
|
||||
if (
|
||||
thread.getType() !== 'mailbox' &&
|
||||
!message.isAuthor() &&
|
||||
!message.isMyselfAuthor() &&
|
||||
!message.isSystemNotification()
|
||||
) {
|
||||
// HERE (remove below line for final version)
|
||||
if (options.incrementUnread) {
|
||||
thread.incrementUnreadCounter();
|
||||
}
|
||||
if (thread.isChat() && options.showNotification) {
|
||||
if (thread.isTwoUserThread() && options.showNotification) {
|
||||
if (
|
||||
!self._isDiscussOpen() &&
|
||||
!config.device.isMobile &&
|
||||
@@ -563,10 +557,10 @@ var MailManager = AbstractService.extend({
|
||||
// while keeping it unread
|
||||
thread.detach({ passively: true });
|
||||
}
|
||||
var query = { isBottomVisible: false };
|
||||
var query = { isVisible: false };
|
||||
self._mailBus.trigger('is_thread_bottom_visible', thread, query);
|
||||
if (!query.isBottomVisible) {
|
||||
self._notifyIncomingMessage(message, query);
|
||||
if (!self.call('bus_service', 'isOdooFocused') || !query.isVisible) {
|
||||
self._notifyIncomingMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -598,7 +592,7 @@ var MailManager = AbstractService.extend({
|
||||
* @param {string} name
|
||||
* @returns {$.Promise<integer>} ID of the created channel
|
||||
*/
|
||||
_createDM: function (name) {
|
||||
_createDMChat: function (name) {
|
||||
var context = _.extend({ isMobile: config.device.isMobile }, session.user_context);
|
||||
return this._rpc({
|
||||
model: 'mail.channel',
|
||||
@@ -620,6 +614,25 @@ var MailManager = AbstractService.extend({
|
||||
args: [channelIDs],
|
||||
}, { shadow: true });
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_fetchMailStateFromServer: function () {
|
||||
var self = this;
|
||||
this._isReady = session.is_bound.then(function () {
|
||||
var context = _.extend(
|
||||
{ isMobile: config.device.isMobile },
|
||||
session.user_context
|
||||
);
|
||||
return self._rpc({
|
||||
route: '/mail/init_messaging',
|
||||
params: { context: context },
|
||||
});
|
||||
}).then(function (result) {
|
||||
self._updateInternalStateFromServer(result);
|
||||
self.call('bus_service', 'startPolling');
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Get previews of the channels
|
||||
*
|
||||
@@ -744,9 +757,9 @@ var MailManager = AbstractService.extend({
|
||||
return false;
|
||||
}
|
||||
if (filter === 'chat') {
|
||||
return thread.isChat();
|
||||
return thread.isTwoUserThread();
|
||||
} else if (filter === 'channels') {
|
||||
return !thread.isChat();
|
||||
return !thread.isTwoUserThread();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -809,6 +822,8 @@ var MailManager = AbstractService.extend({
|
||||
*
|
||||
* @private
|
||||
* @param {string|undefined} [filter]
|
||||
* @returns {$.Promise<Object[]>} resolved with list of object that are
|
||||
* compatible with preview template of mail systray menu
|
||||
*/
|
||||
_getSystrayStaticPreviews: function (filter) {
|
||||
var elems = [];
|
||||
@@ -827,132 +842,29 @@ var MailManager = AbstractService.extend({
|
||||
return $.when(elems);
|
||||
},
|
||||
/**
|
||||
* Initialize the canned responses from the server data
|
||||
* Initialize the internal state of the mail service. Ensure that all
|
||||
* attributes are set before doing any operation on them.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} data
|
||||
* @param {Object[]} [data.shortcodes]
|
||||
* @param {integer} data.shortcodes[i].id
|
||||
* @param {string} data.shortcodes[i].source
|
||||
* @param {string} data.shortcodes[i].substitution
|
||||
*/
|
||||
_initializeCannedResponses: function (data) {
|
||||
var self = this;
|
||||
_.each(data.shortcodes, function (s) {
|
||||
var cannedResponse = _.pick(s, ['id', 'source', 'substitution']);
|
||||
self._cannedResponses.push(cannedResponse);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Initialize the channels from the server, including public, private, DM,
|
||||
* livechat, etc.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} data
|
||||
* @param {Object} [data.channel_slots] contains the data of channels to
|
||||
* initialize, which are grouped by channel type by key of the object
|
||||
* (e.g. list of public channel data are stored in 'channel_channel')
|
||||
* @param {Object[]} [data.channel_slots[i] list of data of channel of type
|
||||
* `i`
|
||||
*/
|
||||
_initializeChannels: function (data) {
|
||||
var self = this;
|
||||
_.each(data.channel_slots, function (channels) {
|
||||
_.each(channels, self._addChannel.bind(self));
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Initialize commands from the server
|
||||
*
|
||||
* @private
|
||||
* @param {Object} data
|
||||
* @param {Object[]} data.commands list of command data from the server
|
||||
*/
|
||||
_initializeCommands: function (data) {
|
||||
this._commands = _.map(data.commands, function (command) {
|
||||
return _.extend({ id: command.name }, command);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {$.Promise}
|
||||
*/
|
||||
_initializeFromServer: function () {
|
||||
var self = this;
|
||||
this._isReady = session.is_bound.then(function () {
|
||||
var context = _.extend(
|
||||
{ isMobile: config.device.isMobile },
|
||||
session.user_context
|
||||
);
|
||||
return self._rpc({
|
||||
route: '/mail/init_messaging',
|
||||
params: { context: context },
|
||||
});
|
||||
}).then(function (result) {
|
||||
self._updateFromServer(result);
|
||||
self.call('bus_service', 'startPolling');
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Initialize the mailboxes, namely 'Inbox', 'Starred',
|
||||
* and 'Moderation Queue' if the user is a moderator of a channel
|
||||
*
|
||||
* @private
|
||||
* @param {Object} data
|
||||
* @param {boolean} [data.is_moderator=false] states whether the user is
|
||||
* moderator of a channel
|
||||
* @param {integer} [data.moderation_counter=0] states the mailbox counter
|
||||
* to set to 'Moderation Queue'
|
||||
* @param {integer} [data.needaction_inbox_counter=0] states the mailbox
|
||||
* counter to set to 'Inbox'
|
||||
* @param {integer} [data.starred_counter=0] states the mailbox counter to
|
||||
* set to 'Starred'
|
||||
*/
|
||||
_initializeMailboxes: function (data) {
|
||||
this._addMailbox({
|
||||
id: 'inbox',
|
||||
name: _t("Inbox"),
|
||||
mailboxCounter: data.needaction_inbox_counter || 0,
|
||||
});
|
||||
this._addMailbox({
|
||||
id: 'starred',
|
||||
name: _t("Starred"),
|
||||
mailboxCounter: data.starred_counter || 0,
|
||||
});
|
||||
|
||||
if (data.is_moderator) {
|
||||
this._addMailbox({
|
||||
id: 'moderation',
|
||||
name: _t("Moderate Messages"),
|
||||
mailboxCounter: data.moderation_counter || 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Initialize mail failures from the server data
|
||||
*
|
||||
* @private
|
||||
* @param {Object} data
|
||||
* @param {Object[]} data.mail_failures data to initialize mail failures
|
||||
* locally
|
||||
*/
|
||||
_initializeMailFailures: function (data) {
|
||||
var self = this;
|
||||
this._mailFailures = _.map(data.mail_failures, function (mailFailureData) {
|
||||
return new MailFailure(self, mailFailureData);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Initialize moderation settings from the server data
|
||||
*
|
||||
* @private
|
||||
* @param {Object} data
|
||||
* @param {boolean} [data.is_moderator=false]
|
||||
* @param {integer[]} [data.moderation_channel_ids]
|
||||
*/
|
||||
_initializeModerationSettings: function (data) {
|
||||
this._moderatedChannelIDs = data.moderation_channel_ids;
|
||||
this._isModerator = data.is_moderator;
|
||||
_initializeInternalState: function () {
|
||||
this._cannedResponses = [];
|
||||
this._mailBus = new Bus(this);
|
||||
this._commands = [];
|
||||
this._discussMenuID = undefined;
|
||||
this._discussOpen = false;
|
||||
this._isMyselfModerator = false;
|
||||
this._mailFailures = [];
|
||||
// list of employees for chatter mentions
|
||||
this._mentionPartnerSuggestions = [];
|
||||
this._messages = [];
|
||||
this._moderatedChannelIDs = [];
|
||||
// # of message received when odoo is out of focus
|
||||
this._outOfFocusUnreadMessageCounter = 0;
|
||||
// partner_ids we have a pinned DM chat with
|
||||
this._pinnedDmPartners = [];
|
||||
// all threads, including channels, DM, mailboxes, document threads, ...
|
||||
this._threads = [];
|
||||
},
|
||||
/**
|
||||
* State whether discuss app is open or not
|
||||
@@ -982,26 +894,46 @@ var MailManager = AbstractService.extend({
|
||||
return self._addChannel(result, options);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Listen on several buses, before doing any action that trigger something
|
||||
* on those buses.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_listenOnBuses: function () {
|
||||
this._mailBus.on('discuss_open', this, this._onDiscussOpen);
|
||||
this.call('bus_service', 'on', 'window_focus', this, this._onWindowFocus);
|
||||
},
|
||||
/**
|
||||
* Creates a new instance of Channel with the given data and options.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} data
|
||||
* @param {Array} [data.channel_type] if set and is 'livechat', the channel
|
||||
* is a Livechat.
|
||||
* @param {Array} [data.direct_partner] if set and is an non-empty array,
|
||||
* the channel is a DM
|
||||
* the channel is a DM chat
|
||||
* @param {Object} [options]
|
||||
* @returns {mail.model.Channel}
|
||||
*/
|
||||
_makeChannel: function (data, options) {
|
||||
if (_.size(data.direct_partner) > 0) {
|
||||
return new DM({
|
||||
return new DMChat({
|
||||
parent: this,
|
||||
data: data,
|
||||
options: options,
|
||||
commands: this._commands
|
||||
});
|
||||
}
|
||||
return new Channel({
|
||||
if (data.channel_type === 'livechat') {
|
||||
return new Livechat({
|
||||
parent: this,
|
||||
data: data,
|
||||
options: options,
|
||||
commands: this._commands,
|
||||
});
|
||||
}
|
||||
return new MultiUserChannel({
|
||||
parent: this,
|
||||
data: data,
|
||||
options: options,
|
||||
@@ -1020,16 +952,13 @@ var MailManager = AbstractService.extend({
|
||||
},
|
||||
/**
|
||||
* shows a popup to notify a new received message.
|
||||
* This will also rename the odoo tab browser if
|
||||
* the user has no focus on it.
|
||||
* This will also rename the browser tab if this is not the active tab.
|
||||
*
|
||||
* @private
|
||||
* @param {mail.model.Message} message message received
|
||||
* @param {Object} options
|
||||
* @param {boolean} options.isDisplayed
|
||||
*/
|
||||
_notifyIncomingMessage: function (message, options) {
|
||||
if (this.call('bus_service', 'isOdooFocused') && options.isDisplayed) {
|
||||
_notifyIncomingMessage: function (message) {
|
||||
if (this.call('bus_service', 'isOdooFocused')) {
|
||||
// no need to notify
|
||||
return;
|
||||
}
|
||||
@@ -1209,7 +1138,7 @@ var MailManager = AbstractService.extend({
|
||||
* Sort previews
|
||||
*
|
||||
* 1. unread,
|
||||
* 2. chat,
|
||||
* 2. two-user thread,
|
||||
* 3. date,
|
||||
*
|
||||
* @private
|
||||
@@ -1219,13 +1148,13 @@ var MailManager = AbstractService.extend({
|
||||
_sortPreviews: function (previews) {
|
||||
var res = previews.sort(function (p1, p2) {
|
||||
var unreadDiff = Math.min(1, p2.unreadCounter) - Math.min(1, p1.unreadCounter);
|
||||
var isChatDiff = p2.isChat - p1.isChat;
|
||||
var isTwoUserThreadDiff = p2.isTwoUserThread - p1.isTwoUserThread;
|
||||
var dateDiff = (!!p2.date - !!p1.date) ||
|
||||
(
|
||||
p2.date &&
|
||||
p2.date.diff(p1.date)
|
||||
);
|
||||
return unreadDiff || isChatDiff || dateDiff;
|
||||
return unreadDiff || isTwoUserThreadDiff || dateDiff;
|
||||
});
|
||||
return res;
|
||||
},
|
||||
@@ -1244,6 +1173,53 @@ var MailManager = AbstractService.extend({
|
||||
return _.isString(name) ? name.toLowerCase() : '';
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Update the canned responses with mail data fetched from the server
|
||||
*
|
||||
* @private
|
||||
* @param {Object} data
|
||||
* @param {Object[]} [data.shortcodes]
|
||||
* @param {integer} data.shortcodes[i].id
|
||||
* @param {string} data.shortcodes[i].source
|
||||
* @param {string} data.shortcodes[i].substitution
|
||||
*/
|
||||
_updateCannedResponsesFromServer: function (data) {
|
||||
var self = this;
|
||||
_.each(data.shortcodes, function (s) {
|
||||
var cannedResponse = _.pick(s, ['id', 'source', 'substitution']);
|
||||
self._cannedResponses.push(cannedResponse);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Update the channels with the mail data fetched from server, including
|
||||
* public, private, DM, livechat, etc.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} data
|
||||
* @param {Object} [data.channel_slots] contains the data of channels to
|
||||
* update, which are grouped by channel type by key of the object
|
||||
* (e.g. list of public channel data are stored in 'channel_channel')
|
||||
* @param {Object[]} [data.channel_slots[i] list of data of channel of type
|
||||
* `i`
|
||||
*/
|
||||
_updateChannelsFromServer: function (data) {
|
||||
var self = this;
|
||||
_.each(data.channel_slots, function (channels) {
|
||||
_.each(channels, self._addChannel.bind(self));
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Update commands from mail data fetched from the server
|
||||
*
|
||||
* @private
|
||||
* @param {Object} data
|
||||
* @param {Object[]} data.commands list of command data from the server
|
||||
*/
|
||||
_updateCommandsFromServer: function (data) {
|
||||
this._commands = _.map(data.commands, function (command) {
|
||||
return _.extend({ id: command.name }, command);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Update internal state from server data (mail/init_messaging rpc result)
|
||||
*
|
||||
@@ -1253,18 +1229,79 @@ var MailManager = AbstractService.extend({
|
||||
* with all the employees
|
||||
* @param {integer} result.menu_id the menu ID of discuss app
|
||||
*/
|
||||
_updateFromServer: function (result) {
|
||||
_updateInternalStateFromServer: function (result) {
|
||||
// commands are needed for channel instantiation
|
||||
this._initializeCommands(result);
|
||||
this._initializeChannels(result);
|
||||
this._initializeModerationSettings(result);
|
||||
this._initializeMailboxes(result);
|
||||
this._initializeMailFailures(result);
|
||||
this._initializeCannedResponses(result);
|
||||
this._updateCommandsFromServer(result);
|
||||
this._updateChannelsFromServer(result);
|
||||
this._updateModerationSettingsFromServer(result);
|
||||
this._updateMailboxesFromServer(result);
|
||||
this._updateMailFailuresFromServer(result);
|
||||
this._updateCannedResponsesFromServer(result);
|
||||
|
||||
this._mentionPartnerSuggestions = result.mention_partner_suggestions;
|
||||
this._discussMenuID = result.menu_id;
|
||||
},
|
||||
/**
|
||||
* Update the mailboxes with mail data fetched from server, namely 'Inbox',
|
||||
* 'Starred', and 'Moderation Queue' if the user is a moderator of a channel
|
||||
*
|
||||
* @private
|
||||
* @param {Object} data
|
||||
* @param {boolean} [data.is_moderator=false] states whether the user is
|
||||
* moderator of a channel
|
||||
* @param {integer} [data.moderation_counter=0] states the mailbox counter
|
||||
* to set to 'Moderation Queue'
|
||||
* @param {integer} [data.needaction_inbox_counter=0] states the mailbox
|
||||
* counter to set to 'Inbox'
|
||||
* @param {integer} [data.starred_counter=0] states the mailbox counter to
|
||||
* set to 'Starred'
|
||||
*/
|
||||
_updateMailboxesFromServer: function (data) {
|
||||
this._addMailbox({
|
||||
id: 'inbox',
|
||||
name: _t("Inbox"),
|
||||
mailboxCounter: data.needaction_inbox_counter || 0,
|
||||
});
|
||||
this._addMailbox({
|
||||
id: 'starred',
|
||||
name: _t("Starred"),
|
||||
mailboxCounter: data.starred_counter || 0,
|
||||
});
|
||||
|
||||
if (data.is_moderator) {
|
||||
this._addMailbox({
|
||||
id: 'moderation',
|
||||
name: _t("Moderate Messages"),
|
||||
mailboxCounter: data.moderation_counter || 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Update mail failures with mail data fetched from the server
|
||||
*
|
||||
* @private
|
||||
* @param {Object} data
|
||||
* @param {Object[]} data.mail_failures data to update mail failures
|
||||
* locally
|
||||
*/
|
||||
_updateMailFailuresFromServer: function (data) {
|
||||
var self = this;
|
||||
this._mailFailures = _.map(data.mail_failures, function (mailFailureData) {
|
||||
return new MailFailure(self, mailFailureData);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Update moderation settings with mail data fetched from server
|
||||
*
|
||||
* @private
|
||||
* @param {Object} data
|
||||
* @param {boolean} [data.is_moderator=false]
|
||||
* @param {integer[]} [data.moderation_channel_ids]
|
||||
*/
|
||||
_updateModerationSettingsFromServer: function (data) {
|
||||
this._isMyselfModerator = data.is_moderator;
|
||||
this._moderatedChannelIDs = data.moderation_channel_ids;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
|
||||
@@ -11,16 +11,12 @@ var MailManager = require('mail.Manager');
|
||||
var MailFailure = require('mail.model.MailFailure');
|
||||
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
MailManager.include({
|
||||
|
||||
start: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.call('bus_service', 'onNotification', this, this._onNotification);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -51,8 +47,9 @@ MailManager.include({
|
||||
*
|
||||
* @private
|
||||
* @param {Object} messageData
|
||||
* @param {Array} messageData.channel_ids list of integers and strings,
|
||||
* where strings for static channels, e.g. 'mailbox_inbox'.
|
||||
* @param {integer[]} messageData.channel_ids channel IDs of this message
|
||||
* (note that 'pending moderation' messages in moderated channels do not
|
||||
* have the moderated channels in this array).
|
||||
*/
|
||||
_handleChannelNotification: function (messageData) {
|
||||
var self = this;
|
||||
@@ -415,29 +412,36 @@ MailManager.include({
|
||||
* @param {Array} data.messages message to display on notification
|
||||
*/
|
||||
_handlePartnerUserConnectionNotification: function (data) {
|
||||
var self=this;
|
||||
var partner_id = data.partner_id;
|
||||
this.call('bus_service', 'sendNotification', data.title, data.message, function(){
|
||||
self.call('mail_service', 'openDmWindow', partner_id);
|
||||
var self = this;
|
||||
var partnerID = data.partner_id;
|
||||
this.call('bus_service', 'sendNotification', data.title, data.message, function ( ){
|
||||
self.call('mail_service', 'openDmWindow', partnerID);
|
||||
});
|
||||
},
|
||||
/**
|
||||
*
|
||||
* On receiving an update on user status (e.g. becoming 'online', 'offline',
|
||||
* 'idle', etc.).
|
||||
*
|
||||
* @private
|
||||
* @param {Object} data partner infos
|
||||
* @param {integer} data.id
|
||||
* @param {string} data.im_status
|
||||
* @param {integer} data.id partner ID
|
||||
* @param {string} data.im_status partner new 'im status' (e.g. 'online')
|
||||
*/
|
||||
_handlePresenceNotification: function (data) {
|
||||
var dm = this.getDmFromPartnerID(data.id);
|
||||
if (dm) {
|
||||
dm.setStatus(data.im_status);
|
||||
this._mailBus.trigger('update_dm_presence', dm);
|
||||
var dmChat = this.getDMChatFromPartnerID(data.id);
|
||||
if (dmChat) {
|
||||
dmChat.setStatus(data.im_status);
|
||||
this._mailBus.trigger('update_dm_presence', dmChat);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_listenOnBuses: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.call('bus_service', 'onNotification', this, this._onNotification);
|
||||
},
|
||||
/**
|
||||
* Update the message notification status of message based on update_message
|
||||
*
|
||||
@@ -481,17 +485,12 @@ MailManager.include({
|
||||
_.each(notifs, function (notif) {
|
||||
var model = notif[0][1];
|
||||
if (model === 'ir.needaction') {
|
||||
// new message in the inbox
|
||||
self._handleNeedactionNotification(notif[1]);
|
||||
} else if (model === 'mail.channel') {
|
||||
// new message in a channel
|
||||
self._handleChannelNotification(notif[1]);
|
||||
} else if (model === 'res.partner') {
|
||||
// channel joined/left, message marked as read/(un)starred,
|
||||
// chat open/closed, moderation specific, etc.
|
||||
self._handlePartnerNotification(notif[1]);
|
||||
} else if (model === 'bus.presence') {
|
||||
// update presence of users
|
||||
self._handlePresenceNotification(notif[1]);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,35 +20,13 @@ var THREAD_WINDOW_WIDTH = 325 + 5; // 5 pixels between windows
|
||||
* windows.
|
||||
*/
|
||||
MailManager.include({
|
||||
custom_events: _.extend({}, MailManager.prototype.custom_events, {
|
||||
close_blank_thread_window: '_onCloseBlankThreadWindow',
|
||||
}),
|
||||
|
||||
// tell where to append thread window
|
||||
THREAD_WINDOW_APPENDTO: 'body',
|
||||
|
||||
start: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this._availableSlotsForThreadWindows = 0;
|
||||
this._hiddenThreadWindows = [];
|
||||
// used to keep dropdown open when closing thread windows
|
||||
this._keepHiddenThreadWindowsDropdownOpen = false;
|
||||
this._spaceLeftForThreadWindows = 0;
|
||||
this._threadWindows = [];
|
||||
// jQuery element for the dropdown of hidden thread windows
|
||||
// see _renderHiddenThreadWindowsDropdown
|
||||
this._$hiddenThreadWindowsDropdown = null;
|
||||
|
||||
this._mailBus
|
||||
.on('update_message', this, this._onUpdateMessage)
|
||||
.on('new_message', this, this._onNewMessage)
|
||||
.on('new_channel', this, this._onNewChannel)
|
||||
.on('is_thread_bottom_visible', this, this._onIsThreadBottomVisible)
|
||||
.on('unsubscribe_from_channel', this, this._onUnsubscribeFromChannel)
|
||||
.on('update_thread_unread_counter', this, this._onUpdateThreadUnreadCounter)
|
||||
.on('update_dm_presence', this, this._onUpdateDmPresence);
|
||||
|
||||
core.bus.on('resize', this, _.debounce(this._repositionThreadWindows.bind(this), 100));
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -73,18 +51,20 @@ MailManager.include({
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Open a DM in a thread window. This is useful when selecting a DM in the
|
||||
* blank thread window, so that it replaces it with the DM window.
|
||||
* Open a DM chat in a thread window. This is useful when selecting a DM
|
||||
* chatin the blank thread window, so that it replaces it with the DM chat
|
||||
* window.
|
||||
*
|
||||
* @param {integer} partnerID
|
||||
*/
|
||||
openDmWindow: function (partnerID) {
|
||||
var dm = this.getDmFromPartnerID(partnerID);
|
||||
if (!dm) {
|
||||
this._openAndDetachDm(partnerID);
|
||||
openDMChatWindowFromBlankThreadWindow: function (partnerID) {
|
||||
var dmChat = this.getDMChatFromPartnerID(partnerID);
|
||||
if (!dmChat) {
|
||||
this._openAndDetachDMChat(partnerID);
|
||||
} else {
|
||||
this.openThreadWindow(dm.getID());
|
||||
this.openThreadWindow(dmChat.getID());
|
||||
}
|
||||
this._closeBlankThreadWindow();
|
||||
},
|
||||
/**
|
||||
* Open the thread window if discuss is not opened
|
||||
@@ -129,29 +109,11 @@ MailManager.include({
|
||||
return thread.fetchMessages();
|
||||
}).then(function () {
|
||||
threadWindow.render();
|
||||
threadWindow.threadWidget.scrollToBottom();
|
||||
// setTimeout to prevent to execute handler on first
|
||||
// scrollTo, which is asynchronous
|
||||
setTimeout(function () {
|
||||
threadWindow.threadWidget.$el.on('scroll', null, _.debounce(function () {
|
||||
if (
|
||||
!threadWindow.isPassive() &&
|
||||
threadWindow.threadWidget.isAtBottom()
|
||||
) {
|
||||
thread.markAsRead();
|
||||
}
|
||||
}, 100));
|
||||
}, 0);
|
||||
if (options.passively) {
|
||||
// mark first unread messages as seen when focusing the
|
||||
// window, then on scroll to bottom as usual
|
||||
threadWindow.$('.o_mail_thread, .o_thread_composer')
|
||||
.one('click', function () {
|
||||
thread.markAsRead();
|
||||
});
|
||||
} else if (
|
||||
!self._areThreadWindowsHidden() &&
|
||||
!thread.isFolded()
|
||||
threadWindow.scrollToBottom();
|
||||
if (
|
||||
!self._areAllThreadWindowsHidden() &&
|
||||
!thread.isFolded() &&
|
||||
!threadWindow.isPassive()
|
||||
) {
|
||||
thread.markAsRead();
|
||||
}
|
||||
@@ -170,6 +132,8 @@ MailManager.include({
|
||||
* @param {integer|string} threadID
|
||||
* @param {Object} options option to be applied on opening thread window, if
|
||||
* the thread is detached
|
||||
* @param {boolean} [options.passively=false] if set, the window will behave
|
||||
* passively.
|
||||
*/
|
||||
updateThreadWindow: function (threadID, options) {
|
||||
var thread = this.getThread(threadID);
|
||||
@@ -198,14 +162,14 @@ MailManager.include({
|
||||
this._threadWindows.splice(this._availableSlotsForThreadWindows-1, 0, threadWindow);
|
||||
},
|
||||
/**
|
||||
* States whether the thread windows are hidden or not.
|
||||
* States whether all the thread windows are hidden or not.
|
||||
* When discuss is open, the thread windows are hidden.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_areThreadWindowsHidden: function () {
|
||||
return this._isDiscussOpen();
|
||||
_areAllThreadWindowsHidden: function () {
|
||||
return $(this.THREAD_WINDOW_APPENDTO).hasClass('o_no_thread_window');
|
||||
},
|
||||
/**
|
||||
* Close the thread window linked to the thread with ID `threadID`.
|
||||
@@ -224,6 +188,15 @@ MailManager.include({
|
||||
threadWindow.destroy();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Close the blank thread window.
|
||||
*/
|
||||
_closeBlankThreadWindow: function () {
|
||||
var blankThreadWindow = this._getBlankThreadWindow();
|
||||
if (blankThreadWindow) {
|
||||
this._closeThreadWindow(blankThreadWindow.getID());
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Compute the number of available slots to display thread windows on the
|
||||
* screen. This is based on the width of the screen, and the width of a
|
||||
@@ -256,8 +229,8 @@ MailManager.include({
|
||||
* Get the blank thread window, which is the special thread window that has
|
||||
* no thread linked to it.
|
||||
*
|
||||
* This is useful in case a DM window may replace the blank thread window,
|
||||
* when we want to open a DM from the blank thread window.
|
||||
* This is useful in case a DM chat window may replace the blank thread
|
||||
* window, when we want to open a DM chat from the blank thread window.
|
||||
*
|
||||
* @private
|
||||
* @returns {mail.ThreadWindow|undefined} the "blank thread" window,
|
||||
@@ -298,6 +271,40 @@ MailManager.include({
|
||||
return threadWindow.getID() === threadID;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_initializeInternalState: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this._availableSlotsForThreadWindows = 0;
|
||||
this._hiddenThreadWindows = [];
|
||||
// used to keep dropdown open when closing thread windows
|
||||
this._keepHiddenThreadWindowsDropdownOpen = false;
|
||||
this._spaceLeftForThreadWindows = 0;
|
||||
this._threadWindows = [];
|
||||
// jQuery element for the dropdown of hidden thread windows
|
||||
// see _renderHiddenThreadWindowsDropdown
|
||||
this._$hiddenThreadWindowsDropdown = null;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_listenOnBuses: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this._mailBus
|
||||
.on('update_message', this, this._onUpdateMessage)
|
||||
.on('new_message', this, this._onNewMessage)
|
||||
.on('new_channel', this, this._onNewChannel)
|
||||
.on('is_thread_bottom_visible', this, this._onIsThreadBottomVisible)
|
||||
.on('unsubscribe_from_channel', this, this._onUnsubscribeFromChannel)
|
||||
.on('update_thread_unread_counter', this, this._onUpdateThreadUnreadCounter)
|
||||
.on('update_dm_presence', this, this._onUpdateDmPresence);
|
||||
|
||||
core.bus.on('resize', this, _.debounce(this._repositionThreadWindows.bind(this), 100));
|
||||
},
|
||||
/**
|
||||
* Make the hidden thread window dropdown menu, that is render it and set
|
||||
* event listener on this dropdown menu DOM element.
|
||||
@@ -371,16 +378,16 @@ MailManager.include({
|
||||
threadWindow.toggleFold(false);
|
||||
},
|
||||
/**
|
||||
* Open and detach the DM in a thread window.
|
||||
* Open and detach the DM chat in a thread window.
|
||||
*
|
||||
* This method assumes that no such DM exists locally, so it is kind of a
|
||||
* "create DM and open DM window" operation
|
||||
* This method assumes that no such DM chat exists locally, so it is kind
|
||||
* of a "create DM chat and open DM chat window" operation
|
||||
*
|
||||
* @private
|
||||
* @param {integer} partnerID
|
||||
* @returns {$.Promise<integer>} resolved with ID of the dm channel
|
||||
* @returns {$.Promise<integer>} resolved with ID of the DM chat
|
||||
*/
|
||||
_openAndDetachDm: function (partnerID) {
|
||||
_openAndDetachDMChat: function (partnerID) {
|
||||
return this._rpc({
|
||||
model: 'mail.channel',
|
||||
method: 'channel_get_and_minimize',
|
||||
@@ -497,7 +504,7 @@ MailManager.include({
|
||||
* @private
|
||||
*/
|
||||
_repositionThreadWindows: function () {
|
||||
if (this._areThreadWindowsHidden()) {
|
||||
if (this._areAllThreadWindowsHidden()) {
|
||||
return;
|
||||
}
|
||||
this._computeAvailableSlotsForThreadWindows(this._threadWindows.length);
|
||||
@@ -532,28 +539,21 @@ MailManager.include({
|
||||
*
|
||||
* @private
|
||||
* @param {mail.model.Message} message
|
||||
* @param {boolean} [scrollBottom=false] if set, thread windows with this
|
||||
* message should scroll to the bottom if the message is visible
|
||||
* @param {Object} options
|
||||
* @param {boolean} [options.keepBottom=false] if set, thread windows with
|
||||
* this message should scroll to the bottom if their bottoms are currently
|
||||
* visible.
|
||||
* @param {boolean} [options.passively=false] if set, thread windows with
|
||||
* this message become passive, so that they are marked as read only when
|
||||
* the focus is on the thread window.
|
||||
*/
|
||||
_updateThreadWindows: function (message, scrollBottom) {
|
||||
var self = this;
|
||||
_updateThreadWindowsFromMessage: function (message, options) {
|
||||
if (this._areAllThreadWindowsHidden()) {
|
||||
return;
|
||||
}
|
||||
_.each(this._threadWindows, function (threadWindow) {
|
||||
if (_.contains(message.getThreadIDs(), threadWindow.getID())) {
|
||||
var thread = self.getThread(threadWindow.getID());
|
||||
var messageVisible = !self._areThreadWindowsHidden() &&
|
||||
!threadWindow.isFolded() &&
|
||||
!threadWindow.isHidden() &&
|
||||
threadWindow.threadWidget.isAtBottom();
|
||||
if (messageVisible && !threadWindow.isPassive()) {
|
||||
thread.markAsRead();
|
||||
}
|
||||
thread.fetchMessages()
|
||||
.then(function () {
|
||||
threadWindow.render();
|
||||
if (scrollBottom && messageVisible) {
|
||||
threadWindow.threadWidget.scrollToBottom();
|
||||
}
|
||||
});
|
||||
threadWindow.update(options);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -562,6 +562,14 @@ MailManager.include({
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called when manually closing the blank thread window.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onCloseBlankThreadWindow: function () {
|
||||
this._closeBlankThreadWindow();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {boolean} open
|
||||
@@ -579,21 +587,21 @@ MailManager.include({
|
||||
/**
|
||||
* Called when someone asks window manager whether the bottom of `thread` is
|
||||
* visible or not. An object `query` is provided in order to reponse on the
|
||||
* key `isDisplayed`.
|
||||
* key `isVisible`.
|
||||
*
|
||||
* @private
|
||||
* @param {mail.model.Thread} thread
|
||||
* @param {Object} query
|
||||
* @param {boolean} query.isBottomVisible write on it
|
||||
* @param {boolean} query.isVisible write on it
|
||||
*/
|
||||
_onIsThreadBottomVisible: function (thread, query) {
|
||||
_.each(this._threadWindows, function (threadWindow) {
|
||||
if (
|
||||
thread.getID() === threadWindow.getID() &&
|
||||
threadWindow.threadWidget.isAtBottom() &&
|
||||
threadWindow.isAtBottom() &&
|
||||
!threadWindow.isHidden()
|
||||
) {
|
||||
query.isBottomVisible = true;
|
||||
query.isVisible = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -617,7 +625,7 @@ MailManager.include({
|
||||
* @param {Object} message
|
||||
*/
|
||||
_onNewMessage: function (message) {
|
||||
this._updateThreadWindows(message, true);
|
||||
this._updateThreadWindowsFromMessage(message, { keepBottom: true, passively: true });
|
||||
},
|
||||
/**
|
||||
* Close the thread window when unsusbscribe from a channel.
|
||||
@@ -677,7 +685,7 @@ MailManager.include({
|
||||
* @param {Object} message
|
||||
*/
|
||||
_onUpdateMessage: function (message) {
|
||||
this._updateThreadWindows(message, false);
|
||||
this._updateThreadWindowsFromMessage(message, { keepBottom: false });
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -132,9 +132,7 @@ var ActivityMenu = Widget.extend({
|
||||
});
|
||||
},
|
||||
/**
|
||||
* When menu clicked update activity preview if counter updated
|
||||
* @private
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onActivityMenuClick: function () {
|
||||
if (!this._isOpen()) {
|
||||
|
||||
@@ -7,6 +7,8 @@ var SystrayMenu = require('web.SystrayMenu');
|
||||
var Widget = require('web.Widget');
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
/**
|
||||
* Menu item appended in the systray part of the navbar
|
||||
*
|
||||
@@ -252,6 +254,7 @@ var MessagingMenu = Widget.extend({
|
||||
// we are opening the dropdown so update its content
|
||||
this._updatePreviews();
|
||||
}
|
||||
this._updatePreviews();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
|
||||
@@ -37,7 +37,7 @@ var ThreadField = AbstractField.extend({
|
||||
this.dp = new concurrency.DropPrevious();
|
||||
|
||||
this._threadWidget = new ThreadWidget(this, {
|
||||
displayOrder: ThreadWidget.ORDER.DESC,
|
||||
displayOrder: ThreadWidget.ORDER.ASC,
|
||||
displayDocumentLinks: false,
|
||||
displayMarkAsRead: false,
|
||||
squashCloseMessages: false,
|
||||
|
||||
@@ -12,8 +12,8 @@ var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
var ORDER = {
|
||||
ASC: 1,
|
||||
DESC: -1,
|
||||
ASC: 1, // visually, chronological order of thread from bottom to top
|
||||
DESC: -1, // visually, chronological order of thread from top to bottom
|
||||
};
|
||||
|
||||
var READ_MORE = _t("read more");
|
||||
@@ -55,7 +55,7 @@ var ThreadWidget = Widget.extend({
|
||||
// options when the thread is enabled (e.g. can send message,
|
||||
// interact on messages, etc.)
|
||||
this._enabledOptions = _.defaults(options || {}, {
|
||||
displayOrder: ORDER.ASC,
|
||||
displayOrder: ORDER.DESC,
|
||||
displayMarkAsRead: true,
|
||||
displayStars: true,
|
||||
displayDocumentLinks: true,
|
||||
@@ -78,20 +78,16 @@ var ThreadWidget = Widget.extend({
|
||||
loadMoreOnScroll: this._enabledOptions.loadMoreOnScroll,
|
||||
};
|
||||
this._selectedMessageID = null;
|
||||
this._currentThreadID = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
clearInterval(this._updateTimestampsInterval);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {mail.model.AbstractMessage[]} messages list of messages of the
|
||||
* thread, ordered by increasing IDs (a higher ID means a more recent
|
||||
* message).
|
||||
* @param {}
|
||||
* @param {mail.model.AbstractThread} thread the thread to render.
|
||||
* @param {Object} [options]
|
||||
* @param {integer} [options.displayOrder=ORDER.ASC] order of displaying
|
||||
* messages in the thread:
|
||||
@@ -99,11 +95,18 @@ var ThreadWidget = Widget.extend({
|
||||
* - ORDER.DESC: last message is at the top of the thread
|
||||
* @param {boolean} [options.displayLoadMore]
|
||||
* @param {boolean} [options.isCreateMode]
|
||||
* @param {boolean} [options.scrollToBottom=false]
|
||||
* @param {boolean} [options.squashCloseMessages]
|
||||
*/
|
||||
render: function (thread, options) {
|
||||
var self = this;
|
||||
|
||||
var shouldScrollToBottomAfterRendering = false;
|
||||
if (this._currentThreadID === thread.getID() && this.isAtBottom()) {
|
||||
shouldScrollToBottomAfterRendering = true;
|
||||
}
|
||||
this._currentThreadID = thread.getID();
|
||||
|
||||
// copy so that reverse do not alter order in the thread object
|
||||
var messages = _.clone(thread.getMessages());
|
||||
|
||||
@@ -115,9 +118,6 @@ var ThreadWidget = Widget.extend({
|
||||
return message.getAttachments();
|
||||
})));
|
||||
|
||||
if (modeOptions.displayOrder === ORDER.DESC) {
|
||||
messages.reverse();
|
||||
}
|
||||
options = _.extend({}, modeOptions, options, {
|
||||
selectedMessageID: this._selectedMessageID,
|
||||
});
|
||||
@@ -160,6 +160,11 @@ var ThreadWidget = Widget.extend({
|
||||
}
|
||||
prevMessage = message;
|
||||
});
|
||||
|
||||
if (modeOptions.displayOrder === ORDER.DESC) {
|
||||
messages.reverse();
|
||||
}
|
||||
|
||||
this.$el.html(QWeb.render('mail.widget.Thread', {
|
||||
thread: thread,
|
||||
displayAuthorMessages: displayAuthorMessages,
|
||||
@@ -175,6 +180,10 @@ var ThreadWidget = Widget.extend({
|
||||
self._insertReadMore($message);
|
||||
});
|
||||
|
||||
if (shouldScrollToBottomAfterRendering) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
if (!this._updateTimestampsInterval) {
|
||||
this.updateTimestampsInterval = setInterval(function () {
|
||||
self._updateTimestamps();
|
||||
@@ -368,11 +377,6 @@ var ThreadWidget = Widget.extend({
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} options
|
||||
|
||||
@@ -33,10 +33,10 @@ var AbstractThreadWindow = Widget.extend({
|
||||
events: {
|
||||
'click .o_thread_window_close': '_onClickClose',
|
||||
'click .o_thread_window_title': '_onClickFold',
|
||||
'click .o_thread_composer': '_onComposerClick',
|
||||
'click .o_composer_text_field': '_onComposerClick',
|
||||
'click .o_mail_thread': '_onThreadWindowClicked',
|
||||
'keydown .o_thread_composer': '_onKeydown',
|
||||
'keypress .o_thread_composer': '_onKeypress',
|
||||
'keydown .o_composer_text_field': '_onKeydown',
|
||||
'keypress .o_composer_text_field': '_onKeypress',
|
||||
},
|
||||
/**
|
||||
* Children of this class must make use of `thread`, which is an object that
|
||||
@@ -50,6 +50,7 @@ var AbstractThreadWindow = Widget.extend({
|
||||
* thread window is linked to. If not set, it is the "blank" thread
|
||||
* window.
|
||||
* @param {Object} [options={}]
|
||||
* @param {mail.model.AbstractThread} [options.thread]
|
||||
*/
|
||||
init: function (parent, thread, options) {
|
||||
this._super(parent);
|
||||
@@ -65,16 +66,19 @@ var AbstractThreadWindow = Widget.extend({
|
||||
this._hidden = false;
|
||||
this._thread = thread || null;
|
||||
|
||||
this._debouncedOnScroll = _.debounce(this._onScroll.bind(this), 100);
|
||||
|
||||
if (!this.hasThread()) {
|
||||
// internal fold state of thread window without any thread
|
||||
this._folded = false;
|
||||
}
|
||||
},
|
||||
start: function () {
|
||||
var self = this;
|
||||
this.$input = this.$('.o_composer_text_field');
|
||||
this.$header = this.$('.o_thread_window_header');
|
||||
|
||||
this.threadWidget = new ThreadWidget(this, {
|
||||
this._threadWidget = new ThreadWidget(this, {
|
||||
displayDocumentLinks: false,
|
||||
displayMarkAsRead: false,
|
||||
displayStars: this.options.displayStars,
|
||||
@@ -89,7 +93,9 @@ var AbstractThreadWindow = Widget.extend({
|
||||
var margin_dir = _t.database.parameters.direction === "rtl" ? "margin-left" : "margin-right";
|
||||
this.$el.css(margin_dir, $.position.scrollbarWidth());
|
||||
}
|
||||
var def = this.threadWidget.replace(this.$('.o_thread_window_content'));
|
||||
var def = this._threadWidget.replace(this.$('.o_thread_window_content')).then(function () {
|
||||
self._threadWidget.$el.on('scroll', self, self._debouncedOnScroll);
|
||||
});
|
||||
return $.when(this._super(), def);
|
||||
},
|
||||
/**
|
||||
@@ -125,8 +131,9 @@ var AbstractThreadWindow = Widget.extend({
|
||||
*/
|
||||
close: function () {},
|
||||
/**
|
||||
* Get the status of the thread, such as the im status of a DM ('online',
|
||||
* 'offline', etc.). If this window has no thread, returns `undefined`.
|
||||
* Get the status of the thread, such as the im status of a DM chat
|
||||
* ('online', 'offline', etc.). If this window has no thread, returns
|
||||
* `undefined`.
|
||||
*
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
@@ -172,6 +179,15 @@ var AbstractThreadWindow = Widget.extend({
|
||||
hasThread: function () {
|
||||
return !! this._thread;
|
||||
},
|
||||
/**
|
||||
* Tells whether the bottom of the thread in the thread window is visible
|
||||
* or not.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isAtBottom: function () {
|
||||
return this._threadWidget.isAtBottom();
|
||||
},
|
||||
/**
|
||||
* State whether the related thread is folded or not. If there are no
|
||||
* thread related to this window, it means this is the "blank" thread
|
||||
@@ -202,13 +218,22 @@ var AbstractThreadWindow = Widget.extend({
|
||||
isHidden: function () {
|
||||
return this._hidden;
|
||||
},
|
||||
/**
|
||||
* States whether the input of the thread window should be displayed or not.
|
||||
* By default, any thread window with a thread needs a composer.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
needsComposer: function () {
|
||||
return this.hasThread();
|
||||
},
|
||||
/**
|
||||
* Render the thread window
|
||||
*/
|
||||
render: function () {
|
||||
this.renderHeader();
|
||||
if (this.hasThread()) {
|
||||
this.threadWidget.render(this._thread, { displayLoadMore: false });
|
||||
this._threadWidget.render(this._thread, { displayLoadMore: false });
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -223,6 +248,12 @@ var AbstractThreadWindow = Widget.extend({
|
||||
this.$header.html(
|
||||
QWeb.render('mail.AbstractThreadWindow.HeaderContent', options));
|
||||
},
|
||||
/**
|
||||
* Scroll to the bottom of the thread in the thread window
|
||||
*/
|
||||
scrollToBottom: function () {
|
||||
this._threadWidget.scrollToBottom();
|
||||
},
|
||||
/**
|
||||
* Toggle the fold state of this thread window. Also update the fold state
|
||||
* of the thread model. If the boolean parameter `folded` is provided, it
|
||||
@@ -244,7 +275,7 @@ var AbstractThreadWindow = Widget.extend({
|
||||
*/
|
||||
updateVisualFoldState: function () {
|
||||
if (!this.isFolded()) {
|
||||
this.threadWidget.scrollToBottom();
|
||||
this._threadWidget.scrollToBottom();
|
||||
this._focusInput();
|
||||
}
|
||||
this._animateFold();
|
||||
@@ -311,13 +342,22 @@ var AbstractThreadWindow = Widget.extend({
|
||||
return this._thread.getID();
|
||||
},
|
||||
/**
|
||||
* Post a message on this thread window
|
||||
* Post a message on this thread window, and auto-scroll to the bottom of
|
||||
* the thread.
|
||||
*
|
||||
* @abstract
|
||||
* @private
|
||||
* @param {Object} messageData
|
||||
*/
|
||||
_postMessage: function (messageData) {},
|
||||
_postMessage: function (messageData) {
|
||||
var self = this;
|
||||
if (!this.hasThread()) {
|
||||
return;
|
||||
}
|
||||
this._thread.postMessage(messageData)
|
||||
.then(function () {
|
||||
self._threadWidget.scrollToBottom();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Update the fold state of the thread.
|
||||
* This function is called when toggling the fold state of this window.
|
||||
@@ -334,6 +374,13 @@ var AbstractThreadWindow = Widget.extend({
|
||||
this._folded = folded;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Warn other mail components that the unread counter has been updated
|
||||
*
|
||||
* @abstract
|
||||
* @private
|
||||
*/
|
||||
_warnUpdatedUnreadCounter: function () {},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
@@ -416,6 +463,14 @@ var AbstractThreadWindow = Widget.extend({
|
||||
_onKeypress: function (ev) {
|
||||
ev.stopPropagation(); // to prevent jquery's blockUI to cancel event
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onScroll: function () {
|
||||
if (this.hasThread() && this.isAtBottom()) {
|
||||
this._thread.markAsRead();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* When a thread window is clicked on, we want to give the focus to the main
|
||||
* input. An exception is made when the user is selecting something.
|
||||
|
||||
@@ -16,7 +16,10 @@ var _t = core._t;
|
||||
*/
|
||||
var ThreadWindow = AbstractThreadWindow.extend({
|
||||
template: 'mail.ThreadWindow',
|
||||
custom_events: AbstractThreadWindow.prototype.custom_events,
|
||||
events: _.extend({}, AbstractThreadWindow.prototype.events, {
|
||||
'click .o_mail_thread': '_onThreadWindowFocus',
|
||||
'click .o_thread_composer': '_onThreadWindowFocus',
|
||||
'click .o_thread_window_expand': '_onClickExpand',
|
||||
}),
|
||||
/**
|
||||
@@ -25,7 +28,8 @@ var ThreadWindow = AbstractThreadWindow.extend({
|
||||
* @override
|
||||
* @param {mail.Manager} parent
|
||||
* @param {mail.model.Thread} [thread = null] if not set, this is a "blank"
|
||||
* thread window. It lets us open a DM by providing the name of a chat.
|
||||
* thread window. It lets us open a DM chat by providing the name of a
|
||||
* user.
|
||||
* @param {Object} [options={}]
|
||||
* @param {boolean} [options.passively=false]
|
||||
*/
|
||||
@@ -34,11 +38,11 @@ var ThreadWindow = AbstractThreadWindow.extend({
|
||||
|
||||
// don't automatically mark unread messages as seen when at the bottom
|
||||
// of the thread
|
||||
this._passive = this.options.passively;
|
||||
this._passive = this.options.passively || false;
|
||||
|
||||
if (!this.hasThread()) {
|
||||
// remembered partner ID of "blank" thread window in order to be
|
||||
// replaced with newly opened DM window
|
||||
// replaced with newly opened DM chat window
|
||||
this.directPartnerID = null;
|
||||
}
|
||||
},
|
||||
@@ -54,10 +58,11 @@ var ThreadWindow = AbstractThreadWindow.extend({
|
||||
var composerDef;
|
||||
if (!this.hasThread()) {
|
||||
this._startWithoutThread();
|
||||
} else if (this._needsComposer()) {
|
||||
} else if (this.needsComposer()) {
|
||||
var basicComposer = new BasicComposer(this, {
|
||||
mentionPartnersRestricted: this._thread.getType() !== 'document_thread',
|
||||
isMini: true
|
||||
isMini: true,
|
||||
thread: this._thread,
|
||||
});
|
||||
basicComposer.on('post_message', this, this._postMessage);
|
||||
basicComposer.once('input_focused', this, function () {
|
||||
@@ -85,6 +90,8 @@ var ThreadWindow = AbstractThreadWindow.extend({
|
||||
close: function () {
|
||||
if (this.hasThread()) {
|
||||
this._thread.close();
|
||||
} else {
|
||||
this.trigger_up('close_blank_thread_window');
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -124,6 +131,24 @@ var ThreadWindow = AbstractThreadWindow.extend({
|
||||
isPassive: function () {
|
||||
return this._passive;
|
||||
},
|
||||
/**
|
||||
* States whether the input of the thread window should be displayed or not.
|
||||
* This is based on the type of the thread:
|
||||
*
|
||||
* Do not display the input in the following cases:
|
||||
*
|
||||
* - no thread related to this window
|
||||
* - window of a mailbox (temp: let us have mailboxes in window mode)
|
||||
* - window of a thread with mass mailing
|
||||
*
|
||||
* Any other threads show the input in the window.
|
||||
*
|
||||
* @override
|
||||
* @returns {boolean}
|
||||
*/
|
||||
needsComposer: function () {
|
||||
return this._super() && !this._thread.isMassMailing();
|
||||
},
|
||||
/**
|
||||
* Turn the thread window in active mode, so that when the bottom of the
|
||||
* thread is visible, it is automatically marked as read.
|
||||
@@ -131,6 +156,42 @@ var ThreadWindow = AbstractThreadWindow.extend({
|
||||
removePassive: function () {
|
||||
this._passive = false;
|
||||
},
|
||||
/**
|
||||
* Set the thread window in passive mode, so that new received message will
|
||||
* keep the thread window as unread until there is focus on the thread
|
||||
* window.
|
||||
*/
|
||||
setPassive: function () {
|
||||
this._passive = true;
|
||||
},
|
||||
/**
|
||||
* Update this thread window
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {boolean} [options.keepBottom=false] if set, this thread window
|
||||
* should scroll to the bottom if it was at the bottom before update
|
||||
* @param {boolean} [options.passively=false] if set, this thread window
|
||||
* becomes passive, so that it is marked as read only when the focus is
|
||||
* on it.
|
||||
*/
|
||||
update: function (options) {
|
||||
var self = this;
|
||||
if (options.passively) {
|
||||
this.setPassive();
|
||||
}
|
||||
var bottomVisible = !this.isFolded() &&
|
||||
!this.isHidden() &&
|
||||
this.isAtBottom();
|
||||
if (bottomVisible && !this.isPassive()) {
|
||||
this._thread.markAsRead();
|
||||
}
|
||||
this._thread.fetchMessages().then(function () {
|
||||
self.render();
|
||||
if (bottomVisible && options.keepBottom) {
|
||||
self.scrollToBottom();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
@@ -155,31 +216,11 @@ var ThreadWindow = AbstractThreadWindow.extend({
|
||||
* @private
|
||||
*/
|
||||
_listenThreadWidget: function () {
|
||||
this.threadWidget
|
||||
this._threadWidget
|
||||
.on('redirect', this, this._onRedirect)
|
||||
.on('redirect_to_channel', this, this._onRedirectToChannel)
|
||||
.on('toggle_star_status', this, this._onToggleStarStatus);
|
||||
},
|
||||
/**
|
||||
* States whether the input of the thread window should be displayed or not.
|
||||
* This is based on the type of the thread:
|
||||
*
|
||||
* Do not display the input in the following cases:
|
||||
*
|
||||
* - no thread related to this window
|
||||
* - window of a mailbox (temp: let us have mailboxes in window mode)
|
||||
* - window of a thread with mass mailing
|
||||
*
|
||||
* Any other threads show the input in the window.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_needsComposer: function () {
|
||||
return this.hasThread() &&
|
||||
(this._thread.getType() !== 'mailbox') &&
|
||||
!this._thread.isMassMailing();
|
||||
},
|
||||
/**
|
||||
* Open this thread window.
|
||||
* This private method exists only for the purpose of providing a callback
|
||||
@@ -190,24 +231,6 @@ var ThreadWindow = AbstractThreadWindow.extend({
|
||||
_open: function () {
|
||||
this.call('mail_service', 'openThreadWindow', this.getID());
|
||||
},
|
||||
/**
|
||||
* Post a message in the thread related to this window.
|
||||
* If this window has no thread, do nothing
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
* @param {Object} messageData
|
||||
*/
|
||||
_postMessage: function (messageData) {
|
||||
var self = this;
|
||||
if (!this.hasThread()) {
|
||||
return;
|
||||
}
|
||||
this._thread.postMessage(messageData)
|
||||
.then(function () {
|
||||
self.threadWidget.scrollToBottom();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@@ -222,10 +245,10 @@ var ThreadWindow = AbstractThreadWindow.extend({
|
||||
},
|
||||
select: function (event, ui) {
|
||||
// remember partner ID so that we can replace this window
|
||||
// with new DM window
|
||||
// with new DM chat window
|
||||
var partnerID = ui.item.id;
|
||||
self.directPartnerID = partnerID;
|
||||
self.call('mail_service', 'openDmWindow', partnerID);
|
||||
self.call('mail_service', 'openDMChatWindowFromBlankThreadWindow', partnerID);
|
||||
},
|
||||
})
|
||||
.focus();
|
||||
@@ -248,7 +271,7 @@ var ThreadWindow = AbstractThreadWindow.extend({
|
||||
_onClickExpand: _.debounce(function (ev) {
|
||||
var self = this;
|
||||
ev.preventDefault();
|
||||
if (this._thread.getType() === 'document_thread') {
|
||||
if (this.hasThread() && this._thread.getType() === 'document_thread') {
|
||||
this.do_action({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: this._thread.getDocumentModel(),
|
||||
@@ -305,6 +328,30 @@ var ThreadWindow = AbstractThreadWindow.extend({
|
||||
this.toggleFold(false);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Override it so that passive thread windows do not mark thread as read
|
||||
* on scroll
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_onScroll: function () {
|
||||
if (this.isPassive()) {
|
||||
return;
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* Called when focusing the thread window (click on thread or composer)
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onThreadWindowFocus: function () {
|
||||
if (this.isPassive()) {
|
||||
this.removePassive();
|
||||
this._thread.markAsRead();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {integer} messageID
|
||||
|
||||
@@ -75,6 +75,7 @@ $o-mail-thread-window-zindex: $zindex-modal + 1 !default;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch; // smooth scrolling in iOS app (Safari)
|
||||
|
||||
.o_thread_date_separator {
|
||||
margin: 0px 0px 15px 0px;
|
||||
.o_thread_date {
|
||||
|
||||
@@ -127,7 +127,6 @@
|
||||
.o_mail_thread {
|
||||
flex: 1 0 0;
|
||||
overflow: auto;
|
||||
padding: 0 0 15px 0;
|
||||
-webkit-overflow-scrolling: touch; // smooth scrolling in iOS app (Safari)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,37 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.o_thread_bottom_free_space {
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.o_thread_tooltip_container {
|
||||
display: inline;
|
||||
position: relative;
|
||||
|
||||
.o_thread_tooltip_content {
|
||||
top: 10px;
|
||||
background: $gray-lighter;
|
||||
padding: 4px;
|
||||
border: solid $gray-lighter 1px;
|
||||
border-radius: 5px;
|
||||
color: gray;
|
||||
width: 300px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
transition: opacity 0.5s;
|
||||
i.fa-exclamation {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
.o_thread_tooltip:hover + .o_thread_tooltip_content {
|
||||
display: inline;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.o_thread_date_separator {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
<div class="o_thread_window_content">
|
||||
</div>
|
||||
<div t-if='widget.hasThread()' class="o_thread_composer o_chat_mini_composer">
|
||||
<div t-if="widget.needsComposer()" class="o_thread_composer o_chat_mini_composer">
|
||||
<input class="o_composer_text_field" t-att-placeholder="widget.options.placeholder"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<t t-set="counter" t-value="starredCounter"/>
|
||||
<t t-call="mail.discuss.SidebarCounter"/>
|
||||
</div>
|
||||
<div t-if="isModerator" t-attf-class="o_mail_discuss_title_main o_mail_discuss_item #{(activeThreadID == 'mailbox_moderation') ? 'o_active': ''}"
|
||||
<div t-if="isMyselfModerator" t-attf-class="o_mail_discuss_title_main o_mail_discuss_item #{(activeThreadID == 'mailbox_moderation') ? 'o_active': ''}"
|
||||
data-thread-id="mailbox_moderation">
|
||||
<span class="o_thread_name"> <i class="fa fa-envelope mr8"/>Moderation Queue</span>
|
||||
<t t-set="counter" t-value="moderationCounter"/>
|
||||
@@ -58,7 +58,7 @@
|
||||
<t t-set="inputPlaceholder">Add a channel</t>
|
||||
</t>
|
||||
|
||||
<t t-set="type" t-value="'dm'"/>
|
||||
<t t-set="type" t-value="'dm_chat'"/>
|
||||
<t t-call="mail.discuss.SidebarTitle">
|
||||
<t t-set="title">Direct Messages</t>
|
||||
<t t-set="icon" t-value="fa-user"/>
|
||||
@@ -133,7 +133,7 @@
|
||||
<input type="text" class="o_input" t-attf-placeholder="#{inputPlaceholder}"/>
|
||||
</div>
|
||||
<t t-foreach="threads" t-as="thread">
|
||||
<t t-set="counter" t-value="thread.getType() === 'mailbox' ? thread.getMailboxCounter() : thread.isChat() ? thread.getUnreadCounter() : thread.getNeedactionCounter()"/>
|
||||
<t t-set="counter" t-value="thread.getType() === 'mailbox' ? thread.getMailboxCounter() : thread.isTwoUserThread() ? thread.getUnreadCounter() : thread.getNeedactionCounter()"/>
|
||||
<div t-if="thread.getType() === type" t-att-data-thread-id="thread.getID()" t-att-title="thread.getName()"
|
||||
t-attf-class="o_mail_discuss_item #{thread.hasUnreadMessages() ? ' o_unread_message' : ''} #{(activeThreadID == thread.getID()) ? 'o_active': ''}">
|
||||
<span class="o_thread_name">
|
||||
@@ -221,7 +221,7 @@
|
||||
<span class="fa fa-inbox"/>
|
||||
<span class="o_tab_title">Inbox</span>
|
||||
</div>
|
||||
<div class="o_mail_mobile_tab" data-type="dm">
|
||||
<div class="o_mail_mobile_tab" data-type="dm_chat">
|
||||
<span class="fa fa-user"/>
|
||||
<span class="o_tab_title">Chat</span>
|
||||
</div>
|
||||
@@ -246,7 +246,7 @@
|
||||
<div t-if="!disableAddThread" class="o_mail_add_thread" t-att-data-type="type">
|
||||
<span t-if="type == 'private' || type == 'public'">#</span>
|
||||
<t t-set="inputPlaceholder" t-if="type == 'private' || type == 'public'">Add a channel</t>
|
||||
<t t-set="inputPlaceholder" t-if="type == 'dm'">Open chat</t>
|
||||
<t t-set="inputPlaceholder" t-if="type == 'dm_chat'">Open chat</t>
|
||||
<input type="text" t-attf-placeholder="#{inputPlaceholder}"/>
|
||||
</div>
|
||||
<t t-foreach="previews" t-as="preview">
|
||||
@@ -266,7 +266,7 @@
|
||||
@param {string} [preview.documentModel]
|
||||
@param {string|integer} preview.id
|
||||
@param {string} [preview.imageSRC]
|
||||
@param {boolean} [preview.isAuthor]
|
||||
@param {boolean} [preview.isMyselfAuthor]
|
||||
@param {moment} [preview.date]
|
||||
@param {string} [preview.author]
|
||||
@param {string} [preview.body]
|
||||
@@ -293,7 +293,7 @@
|
||||
<span class="o_last_message_date"> <t t-esc="preview.date ? preview.date.fromNow() : ''"/> </span>
|
||||
</div>
|
||||
<div t-if="preview.body" class="o_last_message_preview">
|
||||
<t t-if="preview.isAuthor">
|
||||
<t t-if="preview.isMyselfAuthor">
|
||||
<span class="fa fa-mail-reply" role="img" aria-label="Reply"/> You:
|
||||
</t>
|
||||
<t t-elif="preview.author">
|
||||
|
||||
@@ -3,22 +3,13 @@
|
||||
|
||||
<!--
|
||||
@param {mail.model.AbstractThread} thread
|
||||
@param {Object} options
|
||||
@param {boolean} [options.displayEmptyThread]
|
||||
@param {boolean} [options.displayLoadMore]
|
||||
@param {boolean} [options.displayNoMatchFound]
|
||||
@param {integer} [options.displayOrder] 1 or -1
|
||||
@param {Object} ORDER
|
||||
@param {integer} ORDER.ASC=1
|
||||
@param {integer} ORDER.DESC=-1
|
||||
-->
|
||||
<t t-name="mail.widget.Thread">
|
||||
<t t-if="thread.hasMessages()">
|
||||
<t t-set="messages" t-value="thread.getMessages()"/>
|
||||
<t t-if="options.displayLoadMore and options.displayOrder == ORDER.ASC"
|
||||
t-call="mail.widget.Thread.LoadMore"/>
|
||||
<t t-call="mail.widget.Thread.Content"/>
|
||||
<t t-if="options.displayLoadMore and options.displayOrder == ORDER.DESC"
|
||||
t-call="mail.widget.Thread.LoadMore"/>
|
||||
</t>
|
||||
<t t-elif="options.displayEmptyThread">
|
||||
<t t-call="mail.widget.Thread.Empty" />
|
||||
@@ -134,14 +125,90 @@
|
||||
</div>
|
||||
|
||||
<!--
|
||||
@param {mail.model.AbstractMessage[]} messages messages ordered by increasing IDs
|
||||
@param {string|integer} [options.messagesSeparatorPosition] 'top' or
|
||||
message ID, the separator is placed just after this message.
|
||||
@param {mail.model.AbstractThread} thread
|
||||
@param {Object} options
|
||||
@param {integer} [options.displayOrder] 1 or -1 chronological order for the thread messages
|
||||
@param {Object} ORDER
|
||||
@param {integer} ORDER.ASC=1 messages are ordered by increasing IDs, from bottom to top
|
||||
@param {integer} ORDER.DESC=-1 messages are ordered by increasing IDs, from top to bottom
|
||||
|
||||
Ordering of messages in the thread, messages are indexed by increasing IDs:
|
||||
_____________ _____________
|
||||
| | | |
|
||||
| message 1 | | message n |
|
||||
| message 2 | | ... |
|
||||
| ... | | message 2 |
|
||||
| message n | | message 1 |
|
||||
|_____________| |_____________|
|
||||
|
||||
ORDER: DESC ASC
|
||||
|
||||
-->
|
||||
<t t-name="mail.widget.Thread.Content">
|
||||
<t t-if="options.messagesSeparatorPosition == 'top'" t-call="mail.MessagesSeparator"/>
|
||||
<t t-set="messages" t-value="thread.getMessages()"/>
|
||||
<t t-if="options.displayOrder === ORDER.ASC" t-call="mail.widget.Thread.Content.ASC"/>
|
||||
<t t-else="" t-call="mail.widget.Thread.Content.DESC"/>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
@param {mail.model.AbstractThread} thread
|
||||
@param {Object} options
|
||||
@param {boolean} [options.displayLoadMore=false]
|
||||
@param {string|integer} [options.messagesSeparatorPosition] 'top' or
|
||||
message ID, the separator is placed just after this message.
|
||||
|
||||
_____________
|
||||
| |
|
||||
| message n |
|
||||
| ... |
|
||||
| message 2 |
|
||||
| message 1 |
|
||||
|_____________|
|
||||
|
||||
ASC Order
|
||||
|
||||
-->
|
||||
<t t-name="mail.widget.Thread.Content.ASC">
|
||||
<div class="o_mail_thread_content_asc">
|
||||
<t t-if="options.messagesSeparatorPosition == 'top'" t-call="mail.MessagesSeparator"/>
|
||||
<t t-set="messages" t-value="messages.slice().reverse()"/>
|
||||
<t t-call="mail.widget.Thread.Messages"/>
|
||||
<t t-if="options.displayLoadMore" t-call="mail.widget.Thread.LoadMore"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
@param {mail.model.AbstractThread} thread
|
||||
@param {Object} options
|
||||
@param {boolean} [options.displayBottomThreadFreeSpace=false]
|
||||
@param {boolean} [options.displayLoadMore=false]
|
||||
|
||||
_____________
|
||||
| |
|
||||
| message 1 |
|
||||
| message 2 |
|
||||
| ... |
|
||||
| message n |
|
||||
|_____________|
|
||||
|
||||
DESC Order
|
||||
-->
|
||||
<t t-name="mail.widget.Thread.Content.DESC">
|
||||
<div class="o_mail_thread_content_desc">
|
||||
<t t-if="options.displayLoadMore" t-call="mail.widget.Thread.LoadMore"/>
|
||||
<t t-call="mail.widget.Thread.Messages"/>
|
||||
<t t-if="options.displayBottomThreadFreeSpace">
|
||||
<div class="o_thread_bottom_free_space"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
@param {mail.model.AbstractMessage[]} messages messages are ordered based
|
||||
on desired display order
|
||||
-->
|
||||
<t t-name="mail.widget.Thread.Messages">
|
||||
<t t-set="current_day" t-value="0"/>
|
||||
<t t-if="options.displayOrder === ORDER.DESC" t-set="messages" t-value="messages.slice().reverse()"/>
|
||||
<t t-foreach="messages" t-as="message">
|
||||
<div t-if="current_day !== message.getDateDay()" class="o_thread_date_separator">
|
||||
<span class="o_thread_date">
|
||||
@@ -261,7 +328,7 @@
|
||||
<t t-if="message.needsModerationByUser()">
|
||||
<t t-call="mail.MessageModerationDecision"/>
|
||||
</t>
|
||||
<t t-elif="message.isAuthor()">
|
||||
<t t-elif="message.isMyselfAuthor()">
|
||||
<t t-call="mail.MessageModerationFeedback"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
@@ -47,9 +47,9 @@ QUnit.test('mobile basic rendering', function (assert) {
|
||||
"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");
|
||||
discuss.$('.o_mail_mobile_tab[data-type=dm_chat]').click();
|
||||
assert.ok(discuss.$('.o_mail_mobile_tab[data-type=dm_chat]').hasClass('active'),
|
||||
"should be in DMs chat tab");
|
||||
assert.strictEqual(discuss.$('.o_mail_discuss_content .o_mail_no_content').length, 0,
|
||||
"should display the no content message");
|
||||
$('.o_mail_discuss_button_dm').click(); // click to add a channel
|
||||
|
||||
@@ -6,7 +6,7 @@ var mailTestUtils = require('mail.testUtils');
|
||||
var createDiscuss = mailTestUtils.createDiscuss;
|
||||
|
||||
QUnit.module('mail', {}, function () {
|
||||
QUnit.module('Discuss moderation', {
|
||||
QUnit.module('Discuss (Moderation)', {
|
||||
beforeEach: function () {
|
||||
// patch _.debounce and _.throttle to be fast and synchronous
|
||||
this.underscoreDebounce = _.debounce;
|
||||
@@ -196,7 +196,7 @@ QUnit.test('moderator: moderated channel with pending moderation message', funct
|
||||
assert.strictEqual($(moderateAllSelector + '[data-decision="accept"]').length, 1,
|
||||
"there should one moderate button to accept messages pending moderation");
|
||||
assert.strictEqual($(moderateAllSelector + '[data-decision="accept"]').attr('style'),
|
||||
'display: none;', 'the moderate button "Accept" should be invisible by default');
|
||||
'display: none', 'the moderate button "Accept" should be invisible by default');
|
||||
assert.strictEqual($(moderateAllSelector + '[data-decision="reject"]').length, 1,
|
||||
"there should one moderate button to reject messages pending moderation");
|
||||
assert.strictEqual($(moderateAllSelector + '[data-decision="reject"]').attr('style'),
|
||||
@@ -217,11 +217,11 @@ QUnit.test('moderator: moderated channel with pending moderation message', funct
|
||||
"the 'Unselect All' button should not be disabled");
|
||||
// check moderate all buttons updated (visible)
|
||||
assert.strictEqual($(moderateAllSelector + '[data-decision="accept"]').attr('style'),
|
||||
'display: inline-block;', 'the moderate button "Accept" should become visible');
|
||||
'display: inline-block', 'the moderate button "Accept" should become visible');
|
||||
assert.strictEqual($(moderateAllSelector + '[data-decision="reject"]').attr('style'),
|
||||
'display: inline-block;', 'the moderate button "Reject" should become visible');
|
||||
'display: inline-block', 'the moderate button "Reject" should become visible');
|
||||
assert.strictEqual($(moderateAllSelector + '[data-decision="discard"]').attr('style'),
|
||||
'display: inline-block;', 'the moderate button "Discard" should become visible');
|
||||
'display: inline-block', 'the moderate button "Discard" should become visible');
|
||||
|
||||
// 2. go to channel 'general'
|
||||
discuss.$('.o_mail_discuss_item[data-thread-id="1"]').click();
|
||||
|
||||
@@ -11,7 +11,6 @@ var testUtils = require('web.test_utils');
|
||||
var createDiscuss = mailTestUtils.createDiscuss;
|
||||
|
||||
QUnit.module('mail', {}, function () {
|
||||
|
||||
QUnit.module('Discuss', {
|
||||
beforeEach: function () {
|
||||
// patch _.debounce and _.throttle to be fast and synchronous
|
||||
@@ -181,7 +180,7 @@ QUnit.test('@ mention in channel', function (assert) {
|
||||
body: args.kwargs.body,
|
||||
channel_ids: [1],
|
||||
};
|
||||
var notification = [[false, 'mail.channel'], data];
|
||||
var notification = [[false, 'mail.channel', 1], data];
|
||||
objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]);
|
||||
receiveMessageDef.resolve();
|
||||
return $.when(42);
|
||||
@@ -204,7 +203,6 @@ QUnit.test('@ mention in channel', function (assert) {
|
||||
var $input = discuss.$('textarea.o_composer_text_field').first();
|
||||
assert.ok($input.length, "should display a composer input");
|
||||
|
||||
// Simulate '@' typed by user with mocked Window.getSelection
|
||||
// Note: focus is needed in order to trigger rpc 'channel_fetch_listeners'
|
||||
$input.focus();
|
||||
$input.val("@");
|
||||
@@ -309,7 +307,6 @@ QUnit.test('@ mention in channel', function (assert) {
|
||||
assert.strictEqual(discuss.$('.o_thread_message_content a').text(),
|
||||
"@Admin", "should have correct mention link in the message content");
|
||||
|
||||
// Restore window.getSelection
|
||||
discuss.destroy();
|
||||
done();
|
||||
});
|
||||
@@ -360,7 +357,7 @@ QUnit.test('no crash focusout emoji button', function (assert) {
|
||||
});
|
||||
|
||||
QUnit.test('older messages are loaded on scroll', function (assert) {
|
||||
assert.expect(3);
|
||||
assert.expect(10);
|
||||
var done = assert.async();
|
||||
|
||||
var fetchCount = 0;
|
||||
@@ -394,10 +391,15 @@ QUnit.test('older messages are loaded on scroll', function (assert) {
|
||||
services: this.services,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'message_fetch') {
|
||||
assert.step(args.method);
|
||||
fetchCount++;
|
||||
// 1st fetch: inbox initial fetch
|
||||
// 2nd fetch: general initial fetch
|
||||
// 3rd fetch: general load more
|
||||
if (fetchCount === 1) {
|
||||
assert.strictEqual(args.kwargs.limit, 30,
|
||||
"should ask to fetch 30 messages at most");
|
||||
}
|
||||
if (fetchCount === 3) {
|
||||
loadMoreDef.resolve();
|
||||
}
|
||||
@@ -405,6 +407,10 @@ QUnit.test('older messages are loaded on scroll', function (assert) {
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
}).then(function (discuss) {
|
||||
|
||||
assert.verifySteps(['message_fetch'],
|
||||
"should fetch messages once for needaction messages (Inbox)");
|
||||
|
||||
var $general = discuss.$('.o_mail_discuss_item[data-thread-id=1]');
|
||||
assert.strictEqual($general.length, 1,
|
||||
"should have a channel item with id 1");
|
||||
@@ -412,6 +418,9 @@ QUnit.test('older messages are loaded on scroll', function (assert) {
|
||||
// switch to 'general'
|
||||
$general.click();
|
||||
|
||||
assert.verifySteps(['message_fetch', 'message_fetch'],
|
||||
"should fetch a second time for general channel messages (30 last messages)");
|
||||
|
||||
assert.strictEqual(discuss.$('.o_thread_message').length, 30,
|
||||
"should display the 30 messages");
|
||||
|
||||
@@ -421,6 +430,8 @@ QUnit.test('older messages are loaded on scroll', function (assert) {
|
||||
loadMoreDef
|
||||
.then(concurrency.delay.bind(concurrency, 0))
|
||||
.then(function () {
|
||||
assert.verifySteps(['message_fetch', 'message_fetch', 'message_fetch'],
|
||||
"should fetch a third time for general channel messages (5 remaining messages)");
|
||||
assert.strictEqual(discuss.$('.o_thread_message').length, 35,
|
||||
"all messages should now be loaded");
|
||||
|
||||
@@ -621,7 +632,6 @@ QUnit.test('confirm dialog when administrator leave (not chat) channel', functio
|
||||
discuss.destroy();
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('convert emoji sources to unicodes on message_post', function (assert) {
|
||||
@@ -657,7 +667,7 @@ QUnit.test('convert emoji sources to unicodes on message_post', function (assert
|
||||
body: args.kwargs.body,
|
||||
channel_ids: [1],
|
||||
};
|
||||
var notification = [[false, 'mail.channel'], data];
|
||||
var notification = [[false, 'mail.channel', 1], data];
|
||||
objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]);
|
||||
receiveMessageDef.resolve();
|
||||
return $.when(42);
|
||||
@@ -691,6 +701,6 @@ QUnit.test('convert emoji sources to unicodes on message_post', function (assert
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ var testUtils = require('web.test_utils');
|
||||
|
||||
QUnit.module('mail', {}, function () {
|
||||
|
||||
QUnit.module('DocumentThreadWindow', {
|
||||
QUnit.module('ThreadWindow (Document Thread)', {
|
||||
beforeEach: function () {
|
||||
var partnerID = 44;
|
||||
this.data = {
|
||||
|
||||
@@ -155,6 +155,9 @@ MockServer.include({
|
||||
if (args.method === 'channel_fetch_preview') {
|
||||
return $.when(this._mockChannelFetchPreview(args));
|
||||
}
|
||||
if (args.method === 'channel_minimize') {
|
||||
return $.when();
|
||||
}
|
||||
if (args.method === 'channel_seen') {
|
||||
return $.when();
|
||||
}
|
||||
@@ -173,7 +176,7 @@ MockServer.include({
|
||||
if (args.method === 'moderate') {
|
||||
return $.when(this._mockModerate(args));
|
||||
}
|
||||
if (args.method === 'channel_minimize') {
|
||||
if (args.method === 'set_message_done') {
|
||||
return $.when();
|
||||
}
|
||||
return this._super(route, args);
|
||||
|
||||
@@ -6,7 +6,6 @@ var BusService = require('bus.BusService');
|
||||
var Discuss = require('mail.Discuss');
|
||||
var MailService = require('mail.Service');
|
||||
|
||||
var AbstractService = require('web.AbstractService');
|
||||
var AbstractStorageService = require('web.AbstractStorageService');
|
||||
var Class = require('web.Class');
|
||||
var ControlPanel = require('web.ControlPanel');
|
||||
@@ -67,6 +66,7 @@ var MockMailService = Class.extend({
|
||||
return BusService.extend({
|
||||
_poll: function () {}, // Do nothing
|
||||
isOdooFocused: function () { return true; },
|
||||
updateOption: function () {},
|
||||
});
|
||||
},
|
||||
mail_service: function () {
|
||||
|
||||
@@ -8,9 +8,14 @@ var testUtils = require('web.test_utils');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
QUnit.module('mail', {
|
||||
beforeEach: function () {
|
||||
beforeEach: function (assert) {
|
||||
var self = this;
|
||||
|
||||
this.BEFORE_EACH_ASSERTIONS_NUM = 1;
|
||||
|
||||
assert.strictEqual($('.o_thread_window').length, 0,
|
||||
"should have no thread windows open before the test");
|
||||
|
||||
// define channel to link to chat window
|
||||
this.data = {
|
||||
'mail.message': {
|
||||
@@ -34,6 +39,8 @@ QUnit.module('mail', {
|
||||
var widget = new Widget();
|
||||
|
||||
// in non-debug mode, append thread windows in qunit-fixture
|
||||
// note that it does not hide thread window because it uses fixed
|
||||
// position, and qunit-fixture uses absolute...
|
||||
if (params.debug) {
|
||||
self.services.mail_service.prototype.THREAD_WINDOW_APPENDTO = 'body';
|
||||
} else {
|
||||
@@ -50,10 +57,9 @@ QUnit.module('mail', {
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.module('thread_window');
|
||||
|
||||
QUnit.module('ThreadWindow');
|
||||
QUnit.test('close thread window using ESCAPE key', function (assert) {
|
||||
assert.expect(5);
|
||||
assert.expect(5 + this.BEFORE_EACH_ASSERTIONS_NUM);
|
||||
|
||||
var parent = this.createParent({
|
||||
data: this.data,
|
||||
@@ -91,7 +97,7 @@ QUnit.module('mail', {
|
||||
});
|
||||
|
||||
QUnit.test('thread window\'s input can still be focused when the UI is blocked', function (assert) {
|
||||
assert.expect(2);
|
||||
assert.expect(2 + this.BEFORE_EACH_ASSERTIONS_NUM);
|
||||
|
||||
var parent = this.createParent({
|
||||
data: this.data,
|
||||
@@ -124,7 +130,7 @@ QUnit.module('mail', {
|
||||
});
|
||||
|
||||
QUnit.test('emoji popover should open correctly in thread windows', function (assert) {
|
||||
assert.expect(1);
|
||||
assert.expect(1 + this.BEFORE_EACH_ASSERTIONS_NUM);
|
||||
|
||||
var parent = this.createParent({
|
||||
data: this.data,
|
||||
@@ -147,5 +153,6 @@ QUnit.module('mail', {
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,13 +24,17 @@
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/messages/message.js"></script>
|
||||
<!-- threads -->
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/abstract_thread.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/channel.js"></script>
|
||||
<!-- channels -->
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/channel.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/multi_user_channel.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/two_user_channel.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/create_mode_document_thread.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/dm.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/dm_chat.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/document_thread.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/livechat.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/mailbox.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/searchable_thread.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/thread.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/models/threads/thread_with_cache.js"></script>
|
||||
<!-- Widgets -->
|
||||
<!-- thread windows -->
|
||||
<script type="text/javascript" src="/mail/static/src/js/thread_windows/abstract_thread_window.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user