diff options
| author | Alexandre Kühn <aku@odoo.com> | 2018-08-11 20:30:51 +0200 |
|---|---|---|
| committer | Alexandre Kühn <aku@odoo.com> | 2018-08-13 14:17:27 +0200 |
| commit | f2302dfb27dd7f6a0dee7c65ab75b62a01706264 (patch) | |
| tree | d58fef9e2e8a5b98c0680f28fc92e24992236029 /addons/mail/static/src/js/models | |
| parent | 3de97f168956a7c22c3af6b72950cf28554f4139 (diff) | |
[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 (6448420c5dd160470e465dee7729d19d8d5e7bab).
Diffstat (limited to 'addons/mail/static/src/js/models')
| -rw-r--r-- | addons/mail/static/src/js/models/messages/abstract_message.js | 4 | ||||
| -rw-r--r-- | addons/mail/static/src/js/models/messages/message.js | 16 | ||||
| -rw-r--r-- | addons/mail/static/src/js/models/threads/abstract_thread.js | 88 | ||||
| -rw-r--r-- | addons/mail/static/src/js/models/threads/channel.js | 162 | ||||
| -rw-r--r-- | addons/mail/static/src/js/models/threads/dm_chat.js (renamed from addons/mail/static/src/js/models/threads/dm.js) | 41 | ||||
| -rw-r--r-- | addons/mail/static/src/js/models/threads/document_thread.js | 156 | ||||
| -rw-r--r-- | addons/mail/static/src/js/models/threads/livechat.js | 57 | ||||
| -rw-r--r-- | addons/mail/static/src/js/models/threads/mailbox.js | 4 | ||||
| -rw-r--r-- | addons/mail/static/src/js/models/threads/multi_user_channel.js | 56 | ||||
| -rw-r--r-- | addons/mail/static/src/js/models/threads/searchable_thread.js (renamed from addons/mail/static/src/js/models/threads/thread_with_cache.js) | 41 | ||||
| -rw-r--r-- | addons/mail/static/src/js/models/threads/thread.js | 155 | ||||
| -rw-r--r-- | addons/mail/static/src/js/models/threads/two_user_channel.js | 32 |
12 files changed, 474 insertions, 338 deletions
diff --git a/addons/mail/static/src/js/models/messages/abstract_message.js b/addons/mail/static/src/js/models/messages/abstract_message.js index 420ac2da41b..f5cf55f0ef1 100644 --- a/addons/mail/static/src/js/models/messages/abstract_message.js +++ b/addons/mail/static/src/js/models/messages/abstract_message.js @@ -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); }, /** diff --git a/addons/mail/static/src/js/models/messages/message.js b/addons/mail/static/src/js/models/messages/message.js index 0c6480f3264..881a5ae2c97 100644 --- a/addons/mail/static/src/js/models/messages/message.js +++ b/addons/mail/static/src/js/models/messages/message.js @@ -358,14 +358,6 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin 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) * * Usually, if this is true, then this message comes from a document thread, @@ -382,6 +374,14 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin 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. * * @override diff --git a/addons/mail/static/src/js/models/threads/abstract_thread.js b/addons/mail/static/src/js/models/threads/abstract_thread.js index 67929517ed4..fb6695c6a6c 100644 --- a/addons/mail/static/src/js/models/threads/abstract_thread.js +++ b/addons/mail/static/src/js/models/threads/abstract_thread.js @@ -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; @@ -30,6 +36,15 @@ var AbstractThread = Class.extend({ //-------------------------------------------------------------------------- /** + * 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 * * @param {boolean} folded @@ -88,12 +103,6 @@ var AbstractThread = Class.extend({ 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. * * @return {boolean} @@ -102,11 +111,76 @@ var AbstractThread = Class.extend({ 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; diff --git a/addons/mail/static/src/js/models/threads/channel.js b/addons/mail/static/src/js/models/threads/channel.js index a33319c7f1f..3f932ccb8ec 100644 --- a/addons/mail/static/src/js/models/threads/channel.js +++ b/addons/mail/static/src/js/models/threads/channel.js @@ -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(), }); }, /** @@ -252,15 +243,6 @@ var Channel = ThreadWithCache.extend({ 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 * * Note that this is not of the responsibility of the channel @@ -282,18 +264,6 @@ var Channel = ThreadWithCache.extend({ 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 * * @returns {boolean} @@ -308,81 +278,32 @@ var Channel = ThreadWithCache.extend({ * @returns {boolean} */ isModerated: function () { - return this._moderation; + return this._isModerated; }, /** - * States whether the current user is moderator of this channel. + * Tells whether the current user is administrator of the channel. + * Note that there is no administrator for two-user channels * * @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); + isMyselfAdministrator: function () { + return session.uid === this._creatorUID && !this.isTwoUserThread(); }, /** - * Prepare and send a message to the server on this channel + * States whether the current user is moderator of 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. + * @returns {boolean} */ - 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; diff --git a/addons/mail/static/src/js/models/threads/dm.js b/addons/mail/static/src/js/models/threads/dm_chat.js index fba63830a55..767a6e32edd 100644 --- a/addons/mail/static/src/js/models/threads/dm.js +++ b/addons/mail/static/src/js/models/threads/dm_chat.js @@ -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} */ @@ -52,35 +51,13 @@ var DM = Channel.extend({ 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; }); diff --git a/addons/mail/static/src/js/models/threads/document_thread.js b/addons/mail/static/src/js/models/threads/document_thread.js index 831c46939a8..283bcf3ca66 100644 --- a/addons/mail/static/src/js/models/threads/document_thread.js +++ b/addons/mail/static/src/js/models/threads/document_thread.js @@ -58,28 +58,6 @@ var DocumentThread = Thread.extend({ //-------------------------------------------------------------------------- /** - * 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. * @@ -179,59 +157,6 @@ var DocumentThread = Thread.extend({ 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 * * Useful in order to handle message history of the document thread, @@ -260,6 +185,30 @@ var DocumentThread = Thread.extend({ //-------------------------------------------------------------------------- /** + * 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 * * @private @@ -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; diff --git a/addons/mail/static/src/js/models/threads/livechat.js b/addons/mail/static/src/js/models/threads/livechat.js new file mode 100644 index 00000000000..d2aa04f926a --- /dev/null +++ b/addons/mail/static/src/js/models/threads/livechat.js @@ -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; + +}); diff --git a/addons/mail/static/src/js/models/threads/mailbox.js b/addons/mail/static/src/js/models/threads/mailbox.js index 4ba7c4e8d1f..172b5af9e3b 100644 --- a/addons/mail/static/src/js/models/threads/mailbox.js +++ b/addons/mail/static/src/js/models/threads/mailbox.js @@ -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 diff --git a/addons/mail/static/src/js/models/threads/multi_user_channel.js b/addons/mail/static/src/js/models/threads/multi_user_channel.js new file mode 100644 index 00000000000..c1681744127 --- /dev/null +++ b/addons/mail/static/src/js/models/threads/multi_user_channel.js @@ -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; + +}); diff --git a/addons/mail/static/src/js/models/threads/thread_with_cache.js b/addons/mail/static/src/js/models/threads/searchable_thread.js index c82432c64d7..fef42c3ffc4 100644 --- a/addons/mail/static/src/js/models/threads/thread_with_cache.js +++ b/addons/mail/static/src/js/models/threads/searchable_thread.js @@ -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 @@ -39,23 +39,6 @@ var ThreadWithCache = Thread.extend({ //-------------------------------------------------------------------------- /** - * 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] * @param {boolean} [loadMore=false] @@ -129,6 +112,24 @@ var ThreadWithCache = Thread.extend({ //-------------------------------------------------------------------------- /** + * 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 * * @override @@ -202,6 +203,6 @@ var ThreadWithCache = Thread.extend({ _getThreadDomain: function () {}, }); -return ThreadWithCache; +return SearchableThread; }); diff --git a/addons/mail/static/src/js/models/threads/thread.js b/addons/mail/static/src/js/models/threads/thread.js index 5fcd0285a82..c9dda28dd93 100644 --- a/addons/mail/static/src/js/models/threads/thread.js +++ b/addons/mail/static/src/js/models/threads/thread.js @@ -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; @@ -49,13 +42,6 @@ var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, { //-------------------------------------------------------------------------- /** - * 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(), @@ -189,16 +175,6 @@ var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, { */ 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 * * Note that this is not of the responsibility of the thread: it only @@ -223,17 +199,6 @@ var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, { 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,29 +272,61 @@ var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, { markAsPreviewed: function () { this._previewed = true; }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + /** - * Mark the thread as read, which resets the unread counter to 0. - * - * @returns {$.Promise} resolved + * @abstract + * @private + * @return {$.Promise} */ - markAsRead: function () { - if (this._unreadCounter > 0) { - this.resetUnreadCounter(); - } + _fetchMessages: function () { return $.when(); }, /** + * Replace character representations of emojis by their unicode + * representation in the provided HTML string. Note that the provided html + * string is altered by this function. + * + * @param {string} htmlString + * @returns {string} + */ + _generateEmojis: function (htmlString) { + _.each(emojis, function (emoji) { + _.each(emoji.sources, function (source) { + var escapedSource = String(source).replace(/([.*+?=^!:${}()|[\]/\\])/g, '\\$1'); + var regexp = new RegExp("(\\s|^)(" + escapedSource + ")(?=\\s|$)", 'g'); + htmlString = htmlString.replace(regexp, '$1' + emoji.unicode); + }); + }); + return htmlString; + }, + /** + * 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) { + _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 @@ -334,58 +344,18 @@ var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, { 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 - //-------------------------------------------------------------------------- - - /** - * @abstract - * @private - * @return {$.Promise} - */ - _fetchMessages: function () { - return $.when(); - }, - /** - * Replace character representations of emojis by their unicode - * representation in the provided HTML string. Note that the provided html - * string is altered by this function. - * - * @param {string} htmlString - * @returns {string} - */ - _generateEmojis: function (htmlString) { - _.each(emojis, function (emoji) { - _.each(emoji.sources, function (source) { - var escapedSource = String(source).replace(/([.*+?=^!:${}()|[\]/\\])/g, '\\$1'); - var regexp = new RegExp("(\\s|^)(" + escapedSource + ")(?=\\s|$)", 'g'); - htmlString = htmlString.replace(regexp, '$1' + emoji.unicode); - }); + return this._super.apply(this, arguments).then(function () { + return $.when(messageData); }); - return htmlString; }, /** - * Warn on the chat bus that the unread counter has been updated + * 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 || {}; diff --git a/addons/mail/static/src/js/models/threads/two_user_channel.js b/addons/mail/static/src/js/models/threads/two_user_channel.js new file mode 100644 index 00000000000..204fd95eb82 --- /dev/null +++ b/addons/mail/static/src/js/models/threads/two_user_channel.js @@ -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; + +}); |
