diff options
| author | Sébastien Theys <seb@odoo.com> | 2020-06-15 18:12:21 +0000 |
|---|---|---|
| committer | Sébastien Theys <seb@odoo.com> | 2020-06-15 18:12:21 +0000 |
| commit | 3fea5b2136627c1d7e83b2a5e111952fe3eb10a1 (patch) | |
| tree | 5eefad07972a87ce4689b83260f981bc87aadad4 /addons/mail/static/src/js/models | |
| parent | 0a82848606151ef60eb6523e9c4c161171f214e7 (diff) | |
[REF] mail, *: refactor messaging with OWL
This commit is a significant rewriting of client-side discuss, chatter,
chat window, and messaging menu using OWL. The behavior should be broadly
the same, with some slight functional changes here and there.
From a technical standpoint, the code of messaging is mainly organized in 2
main groups of modules:
- models, which are logical entities that depict the client-side state of
messaging as a whole.
- components, which are in charge of displaying information from models.
This refactoring also introduces new JS guidelines regarding folder structure
(/static) and naming rules for JS modules.
Community PR: https://github.com/odoo/odoo/pull/39023
Enterprise PR: https://github.com/odoo/enterprise/pull/6249
Task-1914207
This PR is a collaborative work by Alexandre, Julien, Sébastien and Xavier,
with the precious help of Lucas to speed it up towards the end.
closes odoo/odoo#39023
Related: odoo/enterprise#6249
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
Co-authored-by: Alexandre Kühn <aku@odoo.com>
Co-authored-by: Julien Giannone <jgi@odoo.com>
Co-authored-by: Lucas Perais <lpe@odoo.com>
Co-authored-by: Sébastien Theys <seb@odoo.com>
Co-authored-by: Xavier Dubuc <xdu@odoo.com>
Diffstat (limited to 'addons/mail/static/src/js/models')
19 files changed, 0 insertions, 4620 deletions
diff --git a/addons/mail/static/src/js/models/mail_failure.js b/addons/mail/static/src/js/models/mail_failure.js deleted file mode 100644 index 815c95a034f..00000000000 --- a/addons/mail/static/src/js/models/mail_failure.js +++ /dev/null @@ -1,127 +0,0 @@ -odoo.define('mail.model.MailFailure', function (require) { -"use strict"; - -var Class = require('web.Class'); -var core = require('web.core'); -var Mixins = require('web.mixins'); -var ServicesMixin = require('web.ServicesMixin'); -var time = require('web.time'); - -var _t = core._t; - -var MailFailure = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, { - /** - * @param {mail.Manager} parent - * @param {Object} data - * @param {string} data.failure_type - * @param {string} data.last_message_date - * @param {integer} data.message_id - * @param {string} data.model - * @param {string} data.model_name - * @param {string} data.module_icon - * @param {Array} data.notifications - * @param {string} data.record_name - * @param {integer} data.res_id - * @param {string} data.uuid - */ - init: function (parent, data) { - Mixins.EventDispatcherMixin.init.call(this, arguments); - this.setParent(parent); - - this._documentID = data.res_id; - this._documentModel = data.model; - this._lastMessageDate = moment(); // by default: current datetime - this._messageID = data.message_id; - this._messageType = data.message_type; - this._modelName = data.model_name; - this._moduleIcon = data.module_icon; - this._notifications = data.notifications; - - if (data.last_message_date) { - this._lastMessageDate = moment(time.str_to_datetime(data.last_message_date)); - } - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Get the document model that this mail failure is linked to. - * - * @returns {string} - */ - getDocumentModel: function () { - return this._documentModel; - }, - /** - * Get the document ID that this mail failure is linked to. - * - * @returns {integer} - */ - getDocumentID: function () { - return this._documentID; - }, - /** - * Get the ID of the message that this mail failure is related to. - * - * @returns {integer} - */ - getMessageID: function () { - return this._messageID; - }, - /** - * Get the type of the message that this mail failure is related to. - * - * @returns {string} - */ - getMessageType() { - return this._messageType; - }, - /** - * Get a valid object for the 'mail.preview' template - * - * @returns {Object} - */ - getPreview: function () { - var preview = { - body: this._getPreviewBody(), - date: this._lastMessageDate, - documentID: this._documentID, - documentModel: this._documentModel, - id: 'mail_failure', - imageSRC: this._getPreviewImage(), - title: this._modelName, - }; - return preview; - }, - /** - * Tell whether the mail failure comes from a message in a document. - * - * @returns {boolean} - */ - isLinkedToDocument: function () { - return !!(this._documentModel && this._documentID); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @returns {string} - */ - _getPreviewBody() { - return _t("An error occurred when sending an email."); - }, - /** - * @returns {string} - */ - _getPreviewImage() { - return '/mail/static/src/img/smiley/mailfailure.jpg'; - }, -}); - -return MailFailure; - -}); diff --git a/addons/mail/static/src/js/models/messages/abstract_message.js b/addons/mail/static/src/js/models/messages/abstract_message.js deleted file mode 100644 index b293f6f1db0..00000000000 --- a/addons/mail/static/src/js/models/messages/abstract_message.js +++ /dev/null @@ -1,462 +0,0 @@ -odoo.define('mail.model.AbstractMessage', function (require) { -"use strict"; - -var mailUtils = require('mail.utils'); - -var Class = require('web.Class'); -var core = require('web.core'); -var session = require('web.session'); -var time = require('web.time'); - -var _t = core._t; - -/** - * This is an abstract class for modeling messages in JS. - * The purpose of this interface is to make im_livechat compatible with - * mail.widget.Thread, as this widget was designed to work with messages that - * are instances of mail.model.Messages. - * - * Ideally, im_livechat should also handle mail.model.Message, but this is not - * feasible for the moment, as mail.model.Message requires mail.Manager to work, - * and this module should not leak outside of the backend, hence the use of - * mail.model.AbstractMessage as a work-around. - */ -var AbstractMessage = Class.extend({ - - /** - * @param {Widget} parent - * @param {Object} data - * @param {Array} [data.attachment_ids=[]] - * @param {Array} [data.author_id] - * @param {string} [data.body = ""] - * @param {string} [data.date] the server-format date time of the message. - * If not provided, use current date time for this message. - * @param {integer} data.id - * @param {boolean} [data.is_discussion = false] - * @param {boolean} [data.is_notification = false] - * @param {string} [data.message_type = undefined] - */ - init: function (parent, data) { - this._attachmentIDs = data.attachment_ids || []; - this._body = data.body || ""; - // by default: current datetime - this._date = data.date ? moment(time.str_to_datetime(data.date)) : moment(); - this._id = data.id; - this._isDiscussion = data.is_discussion; - this._isNotification = data.is_notification; - this._serverAuthorID = data.author_id; - this._type = data.message_type || undefined; - - this._processAttachmentURL(); - this._attachmentIDs.forEach(function (attachment) { - attachment.filename = attachment.filename || attachment.name || _t("unnamed"); - }); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Get the list of files attached to this message. - * Note that attachments are stored with server-format - * - * @return {Object[]} - */ - getAttachments: function () { - return this._attachmentIDs; - }, - /** - * Get the server ID (number) of the author of this message - * If there are no author, return -1; - * - * @return {integer} - */ - getAuthorID: function () { - if (!this.hasAuthor()) { - return -1; - } - return this._serverAuthorID[0]; - }, - /** - * Threads do not have an im status by default - * - * @return {undefined} - */ - getAuthorImStatus: function () { - return undefined; - }, - /** - * Get the relative url of the avatar to display next to the message - * - * @abstract - * @return {string} - */ - getAvatarSource: function () { - if (this.hasAuthor()) { - return '/web/image/res.partner/' + this.getAuthorID() + '/image_128'; - } - }, - /** - * Get the body content of this message - * - * @return {string} - */ - getBody: function () { - return this._body; - }, - /** - * @return {moment} - */ - getDate: function () { - return this._date; - }, - /** - * Get the date day of this message - * - * @return {string} - */ - getDateDay: function () { - var date = this.getDate().format('YYYY-MM-DD'); - if (date === moment().format('YYYY-MM-DD')) { - return _t("Today"); - } else if (date === moment().subtract(1, 'days').format('YYYY-MM-DD')) { - return _t("Yesterday"); - } - return this.getDate().format('LL'); - }, - /** - * Get the name of the author, if there is an author of this message - * If there are no author of this message, returns 'null' - * - * @return {string} - */ - getDisplayedAuthor: function () { - return this.hasAuthor() ? this._getAuthorName() : null; - }, - /** - * Get the server ID (number) of this message - * - * @override - * @return {integer} - */ - getID: function () { - return this._id; - }, - /** - * Get the list of images attached to this message. - * Note that attachments are stored with server-format - * - * @return {Object[]} - */ - getImageAttachments: function () { - return _.filter(this.getAttachments(), function (attachment) { - return attachment.mimetype && attachment.mimetype.split('/')[0] === 'image'; - }); - }, - /** - * Get the list of non-images attached to this message. - * Note that attachments are stored with server-format - * - * @return {Object[]} - */ - getNonImageAttachments: function () { - return _.difference(this.getAttachments(), this.getImageAttachments()); - }, - /** - * Gets the class to use as the notification icon. - * - * @returns {string} - */ - getNotificationIcon() { - if (!this.hasNotificationsError()) { - return 'fa fa-envelope-o'; - } - return 'fa fa-envelope'; - }, - /** - * Gets the list of notifications of this message, in no specific order. - * By default messages do not have notifications. - * - * @returns {Object[]} - */ - getNotifications() { - return []; - }, - /** - * Gets the text to display next to the notification icon. - * - * @returns {string} - */ - getNotificationText() { - return ''; - }, - /** - * Get the time elapsed between sent message and now - * - * @return {string} - */ - getTimeElapsed: function () { - return mailUtils.timeFromNow(this.getDate()); - }, - /** - * Get the type of message (e.g. 'comment', 'email', 'notification', ...) - * By default, messages are of type 'undefined' - * - * @override - * @return {string|undefined} - */ - getType: function () { - return this._type; - }, - /** - * State whether this message contains some attachments. - * - * @override - * @return {boolean} - */ - hasAttachments: function () { - return this.getAttachments().length > 0; - }, - /** - * State whether this message has an author - * - * @return {boolean} - */ - hasAuthor: function () { - return !!(this._serverAuthorID && this._serverAuthorID[0]); - }, - /** - * State whether this message has an email of its sender. - * By default, messages do not have any email of its sender. - * - * @return {string} - */ - hasEmailFrom: function () { - return false; - }, - /** - * State whether this image contains images attachments - * - * @return {boolean} - */ - hasImageAttachments: function () { - return _.some(this.getAttachments(), function (attachment) { - return attachment.mimetype && attachment.mimetype.split('/')[0] === 'image'; - }); - }, - /** - * State whether this image contains non-images attachments - * - * @return {boolean} - */ - hasNonImageAttachments: function () { - return _.some(this.getAttachments(), function (attachment) { - return !(attachment.mimetype && attachment.mimetype.split('/')[0] === 'image'); - }); - }, - /** - * States whether this message has some notifications. - * - * @returns {boolean} - */ - hasNotifications() { - return this.getNotifications().length > 0; - }, - /** - * States whether this message has notifications that are in error. - * - * @returns {boolean} - */ - hasNotificationsError() { - return this.getNotifications().some(notif => - notif.notification_status === 'exception' || - notif.notification_status === 'bounce' - ); - }, - /** - * State whether this message originates from a channel. - * By default, messages do not originate from a channel. - * - * @override - * @return {boolean} - */ - originatesFromChannel: function () { - return false; - }, - /** - * State whether this message has a subject - * By default, messages do not have any subject. - * - * @return {boolean} - */ - hasSubject: function () { - return false; - }, - /** - * State whether this message is empty - * - * @return {boolean} - */ - isEmpty: function () { - return !this.hasTrackingValues() && - !this.hasAttachments() && - !this.getBody(); - }, - /** - * By default, messages do not have any subtype description - * - * @return {boolean} - */ - hasSubtypeDescription: function () { - return false; - }, - /** - * State whether this message contains some tracking values - * By default, messages do not have any tracking values. - * - * @return {boolean} - */ - hasTrackingValues: function () { - return false; - }, - /** - * State whether this message is a discussion - * - * @return {boolean} - */ - isDiscussion: function () { - return this._isDiscussion; - }, - /** - * State whether this message is linked to a document thread - * By default, messages are not linked to a document thread. - * - * @return {boolean} - */ - isLinkedToDocumentThread: function () { - return false; - }, - /** - * State whether this message is needaction - * By default, messages are not needaction. - * - * @return {boolean} - */ - isNeedaction: function () { - return false; - }, - /** - * State whether this message is a note (i.e. a message from "Log note") - * - * @return {boolean} - */ - isNote: function () { - return this._isNote; - }, - /** - * State whether this message is a notification - * - * User notifications are defined as either - * - notes - * - pushed to user Inbox or email through classic notification process - * - not linked to any document, meaning model and res_id are void - * - * This is useful in order to display white background for user - * notifications in chatter - * - * @returns {boolean} - */ - isNotification: function () { - return this._isNotification; - }, - /** - * State whether this message is starred - * By default, messages are not starred. - * - * @return {boolean} - */ - isStarred: function () { - return false; - }, - /** - * State whether this message is a system notification - * By default, messages are not system notifications - * - * @override - * @return {boolean} - */ - isSystemNotification: function () { - return false; - }, - /** - * States whether the current message needs moderation in general. - * By default, messages do not require any moderation. - * - * @returns {boolean} - */ - needsModeration: function () { - return false; - }, - /** - * @params {integer[]} attachmentIDs - */ - removeAttachments: function (attachmentIDs) { - this._attachmentIDs = _.reject(this._attachmentIDs, function (attachment) { - return _.contains(attachmentIDs, attachment.id); - }); - }, - /** - * State whether this message should redirect to the author - * when clicking on the author of this message. - * - * Do not redirect on author clicked of self-posted messages. - * - * @return {boolean} - */ - shouldRedirectToAuthor: function () { - return !this._isMyselfAuthor(); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Get the name of the author of this message. - * If there are no author of this messages, returns '' (empty string). - * - * @private - * @returns {string} - */ - _getAuthorName: function () { - if (!this.hasAuthor()) { - return ""; - } - return this._serverAuthorID[1]; - }, - /** - * State whether the current user is the author of this message - * - * @private - * @return {boolean} - */ - _isMyselfAuthor: function () { - return this.hasAuthor() && (this.getAuthorID() === session.partner_id); - }, - /** - * Compute url of attachments of this message - * - * @private - */ - _processAttachmentURL: function () { - _.each(this.getAttachments(), function (attachment) { - attachment.url = '/web/content/' + attachment.id + '?download=true'; - }); - }, - -}); - -return AbstractMessage; - -}); diff --git a/addons/mail/static/src/js/models/messages/message.js b/addons/mail/static/src/js/models/messages/message.js deleted file mode 100644 index 1e3ae8c5289..00000000000 --- a/addons/mail/static/src/js/models/messages/message.js +++ /dev/null @@ -1,806 +0,0 @@ -odoo.define('mail.model.Message', function (require) { -"use strict"; - -var emojis = require('mail.emojis'); -var AbstractMessage = require('mail.model.AbstractMessage'); -var mailUtils = require('mail.utils'); - -var core = require('web.core'); -var Mixins = require('web.mixins'); -var ServicesMixin = require('web.ServicesMixin'); -var session = require('web.session'); - -var _t = core._t; - -/** - * This is the main class for the modeling messages in JS. - * Such messages are stored in the mail manager, and any piece of JS code whose - * logic relies on threads must ideally interact with such objects. - */ -var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin, { - /** - * @override - * @param {web.Manager} parent - * @param {Object} data - * @param {string} [data.body = ""] - * @param {(string|integer)[]} [data.channel_ids] - * @param {string} [data.email_from] - * @param {Array} [data.history_partner_ids = []] - * @param {string} [data.info] - * @param {string} [data.model] - * @param {string} [data.moderation_status='accepted'] - * @param {string} [data.module_icon] - * @param {Array} [data.needaction_partner_ids = []] - * @param {Object[]} [data.notifications] - * @param {string} [data.record_name] - * @param {integer} [data.res_id] - * @param {Array} [data.starred_partner_ids = []] - * @param {string} [data.subject] - * @param {string} [data.subtype_description] - * @param {Object[]} [data.tracking_value_ids] - * @param {Object[]} emojis - */ - init: function (parent, data, emojis) { - this._super.apply(this, arguments); - - Mixins.EventDispatcherMixin.init.call(this, arguments); - this.setParent(parent); - - this._setInitialData(data); - - this._processBody(emojis); - this._processMailboxes(); - this._processModeration(); - this._processDocumentThread(); - this._processTrackingValues(); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - * @return {string|undefined} - */ - getAuthorImStatus: function () { - if (!this.hasAuthor()) { - return undefined; - } - return this.call('mail_service', 'getImStatus', { partnerID: this.getAuthorID() }); - }, - /** - * Get the name of the author of this message - * If there are no author, return "". - * - * @return {string} - */ - getAuthorName: function () { - return this._getAuthorName(); - }, - /** - * Get the relative url of the avatar to display next to the message - * - * @override - * @return {string} - */ - getAvatarSource: function () { - if (this._isOdoobotAuthor()) { - return '/mail/static/src/img/odoobot.png'; - } else if (this.hasAuthor()) { - return '/web/image/res.partner/' + this.getAuthorID() + '/image_128'; - } else if (this.getType() === 'email') { - return '/mail/static/src/img/email_icon.png'; - } - return '/mail/static/src/img/smiley/avatar.jpg'; - }, - /** - * Get the text to display for the author of the message - * - * Rule of precedence for the displayed author:: - * - * author name > sender email > "anonymous" - * - * @override - * @return {string} - */ - getDisplayedAuthor: function () { - var res = this._super.apply(this, arguments); - if (res) { - return res; - } else { - return this.hasEmailFrom() ? this.getEmailFrom() : - _t("Anonymous"); - } - }, - /** - * Get the ID of the document that this message is linked. - * - * @override - * @return {integer} - */ - getDocumentID: function () { - return this._documentID; - }, - /** - * Get the model of the document that this message is linked. - * If this message is not linked to a document, returns "". - * - * @override - * @return {string} - */ - getDocumentModel: function () { - return this._documentModel; - }, - /** - * Get the name of the document that this message is linked. - * If this message is not linked to a document, returns "". - * - * It uses the name of an existing document thread, in order to have the - * updated document name. If there is no document thread yet, use the - * currently aware document name. - * - * As a result, this method should ideally be called internally, in order - * to avoid having different names for messages linked to a same modified - * document. - * - * @override - * @return {string} - */ - getDocumentName: function () { - if (!this.isLinkedToDocumentThread()) { - return ""; - } - var model = this.getDocumentModel(); - var id = this.getDocumentID(); - var documentThread = this.call('mail_service', 'getDocumentThread', model, id); - if (documentThread) { - this._documentName = documentThread.getName(); - } - return this._documentName; - }, - /** - * Get the email of the sender of this message. - * If this email has no sender email, returns "". - * - * @return {string} - */ - getEmailFrom: function () { - if (!this.hasEmailFrom()) { - return ""; - } - return this._emailFrom; - }, - /** - * @override - */ - getNotifications() { - return Object.values(this._notifications); - }, - /** - * Get the ID of the channel that this message originates from. - * If this message does not originate from a channel, returns `-1`. - * - * @override - * @return {integer} - */ - getOriginChannelID: function () { - if (!this.originatesFromChannel()) { - return -1; - } - return this._documentID; - }, - /** - * Get the name of the channel that this message originates from. - * If this message does not originate from a channel, returns "". - * - * @override - * @returns {string} - */ - getOriginChannelName: function () { - if (!this.originatesFromChannel()) { - return ""; - } - var originChannelID = this.getOriginChannelID(); - var channel = originChannelID && this.call( - 'mail_service', - 'getChannel', - originChannelID); - if (!channel) { - return ""; - } - return channel.getName(); - }, - /** - * Returns the information required to render the preview of this channel. - * - * @return {Object} - */ - getPreview: function () { - var id, title; - if (this.isLinkedToDocumentThread()) { - id = this.getDocumentModel() + '_' + this.getDocumentID(); - title = this.getDocumentName(); - } else { - id = 'mailbox_inbox'; - title = this.hasSubject() ? this.getSubject() : this.getDisplayedAuthor(); - } - return { - author: this.getDisplayedAuthor(), - body: mailUtils.htmlToTextContentInline(this.getBody()), - date: this.getDate(), - documentModel: this.getDocumentModel(), - documentID: this.getDocumentID(), - id: id, - imageSRC: this._getModuleIcon() || this.getAvatarSource(), - messageID: this.getID(), - status: this.status, - title: title, - isLinkedToDocumentThread: this.isLinkedToDocumentThread(), - }; - }, - /** - * Get the subject of this message - * If this message has no subject, returns "". - * - * @override - * @return {string} - */ - getSubject: function () { - if (!this.hasSubject()) { - return ""; - } - return this._subject; - }, - /** - * @override - * @return {string} - */ - getSubtypeDescription: function () { - return this._subtypeDescription; - }, - /** - * Get the list of thread IDs that this message is linked to - * If this message is not linked to a thread, returns 'undefined' - * - * @return {string[]|undefined} list of thread IDs, if any - */ - getThreadIDs: function () { - return this._threadIDs; - }, - /** - * Get the tracking values of this message - * If this message has no tracking values, returns 'undefined' - * - * @override - * @return {Object[]|undefined} - */ - getTrackingValues: function () { - if (!this.hasTrackingValues()) { - return undefined; - } - return this._trackingValueIDs; - }, - /** - * @override - * @return {string} - */ - getURL: function () { - return session.url('/mail/view?message_id=' + this._id); - }, - /** - * State whether this message has an email of its sender. - * - * @override - * @return {string} - */ - hasEmailFrom: function () { - return !!(this._emailFrom); - }, - /** - * State whether this message has a subject - * - * @return {boolean} - */ - hasSubject: function () { - return !!(this._subject); - }, - /** - * @override - * @return {boolean} - */ - hasSubtypeDescription: function () { - return !!(this._subtypeDescription); - }, - /** - * State whether this message contains some tracking values - * - * @override - * @return {boolean} - */ - hasTrackingValues: function () { - return !!(this._trackingValueIDs && (this._trackingValueIDs.length > 0)); - }, - /** - * 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, - * but the document model could be a channel. In that case, the document - * resID tells the channel that this message originally comes from. - * - * To detect whether the message comes from a channel, see the method - * 'originatesFromChannel'. - * - * @override - * @return {boolean} - */ - isLinkedToDocumentThread: function () { - return !!(this._documentModel !== 'mail.channel' && this._documentID && this._type !== 'user_notification'); - }, - /** - * 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 - * @returns {boolean} - */ - needsModeration: function () { - return this._moderationStatus === 'pending_moderation'; - }, - /** - * States whether the current message needs moderation by the current user. - * Such a message should be in the moderation mailbox. - * - * @returns {boolean} - */ - needsModerationByUser: function () { - return _.contains(this._threadIDs, 'mailbox_moderation'); - }, - /** - * State whether this message is needaction - * - * @override - * @returns {boolean} - */ - isNeedaction: function () { - return _.contains(this._threadIDs, 'mailbox_inbox'); - }, - /** - * State whether this message is starred - * - * @override - * @returns {boolean} - */ - isStarred: function () { - return _.contains(this._threadIDs, 'mailbox_starred'); - }, - /** - * State whether this message is a system notification - * - * @override - * @returns {boolean} - */ - isSystemNotification: function () { - return ( - this.getType() === 'notification' && - this._documentModel === 'mail.channel' - ) || this._isTransient(); - }, - /** - * States whether the message originates from a channel or not - * - * @returns {boolean} - */ - originatesFromChannel: function () { - return this._documentModel === 'mail.channel'; - }, - /** - * Unregister thread with ID `threadID` from this message - * - * @param {string|integer} threadID ID of thread - */ - removeThread: function (threadID) { - this._threadIDs = _.without(this._threadIDs, threadID); - }, - /** - * Update the moderation status of the message, so that it is now accepted - * or rejected. When the message is accepted, it may be linked to more - * threads, which is the case for relay channels on moderated channels. - * - * @param {string} newModerationStatus ['accepted', 'rejected'] - * @param {Object} [options] - * @param {Object} [options.additionalThreadIDs] contains additional thread - * IDs to be registered on the message. - */ - setModerationStatus: function (newModerationStatus, options) { - var self = this; - if (newModerationStatus === this._moderationStatus) { - return; - } - this._moderationStatus = newModerationStatus; - if (newModerationStatus === 'accepted' && options) { - _.each(options.additionalThreadIDs, function (threadID) { - self._addThread(threadID); - }); - } - this._warnMessageModerated(); - }, - /** - * Sets notifications based on data from the server. - * - * @param {Object} notifications - */ - setNotifications(notifications) { - this._notifications = notifications; - }, - /** - * Set whether the message is starred or not. - * If it is starred, the message is moved to the "Starred" mailbox. - * Note that this function only applies it locally, the server is not aware - * - * @param {boolean} starred if set, the message is starred - */ - setStarred: function (starred) { - if (starred) { - this._addThread('mailbox_starred'); - } else { - this.removeThread('mailbox_starred'); - } - }, - /** - * State whether this message should display the subject - * - * @return {boolean} - */ - shouldDisplaySubject: function () { - return this.hasSubject() && - this.getType() !== 'notification' && - !this.originatesFromChannel(); - }, - /** - * State whether this message should redirect to the author - * when clicking on the author of this message. - * - * Do not redirect on author clicked of self-posted or Odoobot messages - * (note: Odoobot is the default author of transient messages) - * - * @override - * @return {boolean} - */ - shouldRedirectToAuthor: function () { - return this._super.apply(this, arguments) && !this._isOdoobotAuthor(); - }, - /** - * Toggle the star status of the message - * - * It relies on the star status of the message from the date of the server. - * The star status is updated from a 'toggle_star' notification on the - * longpoll bus - * - * @see {mail.Manager.Notification} for the receipt of 'toggle_star' - * notification after this rpc. - * - * @return {Promise} - */ - toggleStarStatus: function () { - return this._rpc({ - model: 'mail.message', - method: 'toggle_message_starred', - args: [[this._id]], - }); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Register a thread to the message - * Useful when you mark a message as 'to do'. - * This message will be available in 'Starred' mailbox. - * - * @private - * @param {string|integer} threadID - */ - _addThread: function (threadID) { - if (!this._threadIDs) { - this._threadIDs = []; - } - this._threadIDs.push(threadID); - this._threadIDs = _.uniq(this._threadIDs); - }, - /** - * Get the name of the author of this message. - * If Odoobot is the author (default author for transient messages), - * returns 'Odoobot'. - * - * @override - * @private - * @returns {string} - */ - _getAuthorName: function () { - if (this._isOdoobotAuthor()) { - return "OdooBot"; - } - return this._super.apply(this, arguments); - }, - /** - * @private - * @return {string} - */ - _getModuleIcon: function () { - return this._moduleIcon; - }, - /** - * State if the author of this message is OdooBot - * This is the default author for transient messages. - * - * @private - * @return {boolean} - */ - _isOdoobotAuthor: function () { - return this._serverAuthorID && - this._serverAuthorID[0] === this.call('mail_service', 'getOdoobotID')[0]; - }, - /** - * State whether the message is transient or not - * - * @private - * @return {boolean} - */ - _isTransient: function () { - return this._info === 'transient_message'; - }, - /** - * Convert the server-format body of the message to the client-format. - * Basically, it processes emojis and url. - * - * @private - */ - _processBody: function () { - var self = this; - - _.each(emojis, function (emoji) { - var unicode = emoji.unicode; - var regexp = new RegExp("(?:^|\\s|<[a-z]*>)(" + unicode + ")(?=\\s|$|</[a-z]*>)", 'g'); - var originalBody = self._body; - self._body = self._body.replace(regexp, - ' <span class="o_mail_emoji">' + unicode + '</span> '); - // Idiot-proof limit. If the user had the amazing idea of - // copy-pasting thousands of emojis, the image rendering can lead - // to memory overflow errors on some browsers (e.g. Chrome). Set an - // arbitrary limit to 200 from which we simply don't replace them - // (anyway, they are already replaced by the unicode counterpart). - if (_.str.count(self._body, 'o_mail_emoji') > 200) { - self._body = originalBody; - } - }); - - // add anchor tags to urls - self._body = mailUtils.parseAndTransform(self._body, mailUtils.addLink); - }, - /** - * If the message is linked to a document, link this message to the related - * document thread. If there is no document thread yet, create it - */ - _processDocumentThread: function () { - if (!this.isLinkedToDocumentThread()) { - return; - } - var resModel = this.getDocumentModel(); - var resID = this.getDocumentID(); - var documentThread = this.call('mail_service', 'getOrAddDocumentThread', { - resModel: resModel, - resID: resID, - name: this.getDocumentName(), - }); - this._threadIDs.push(documentThread.getID()); - }, - /** - * Set the appropriate mailboxes to this message based on server data - * - * @private - */ - _processMailboxes: function () { - if (_.contains(this._needactionPartnerIDs, session.partner_id)) { - this._setNeedaction(true); - } - if (_.contains(this._starredPartnerIDs, session.partner_id)) { - this.setStarred(true); - } - if (_.contains(this._historyPartnerIDs, session.partner_id)) { - this._setHistory(true); - } - if ( - this.originatesFromChannel() && - _.contains( - this.call('mail_service', 'getModeratedChannelIDs'), - this.getOriginChannelID() - ) && - this.needsModeration() - ) { - this._setModeratedByUser(true); - } - }, - /** - * Do some extra processing at message init, related to the - * moderated status of the message. - * - * If the message needs moderation, it is not yet linked to the - * moderated channel server-side. Therefore, it is not registered - * in the list of threadIDs, as it is built on server-side information - * at message initialisation (using data.channel_ids). - * - * Since the web client uses the list of thread IDs to show visually the - * message in a thread, we should hack the response of the server so that - * it assumes the message really belongs to this thread. - * - * @private - */ - _processModeration: function () { - if (this.needsModeration()) { - // the message is not linked to the moderated channel on the - // server, therefore this message has not this channel in - // channel_ids. Here, just to show this message in the channel - // visually, it links this message to the channel - this._threadIDs.push(this.getOriginChannelID()); - } - }, - /** - * Process the tracking values on message creation, which - * basically format date to the local only once by message - * - * Cannot be done in preprocess, since it alter the original value - * - * @private - */ - _processTrackingValues: function () { - if (this.hasTrackingValues()) { - _.each(this.getTrackingValues(), function (trackingValue) { - if (trackingValue.field_type === 'datetime') { - if (trackingValue.old_value) { - trackingValue.old_value = - moment - .utc(trackingValue.old_value) - .local() - .format('LLL'); - } - if (trackingValue.new_value) { - trackingValue.new_value = - moment - .utc(trackingValue.new_value) - .local() - .format('LLL'); - } - } else if (trackingValue.field_type === 'date') { - if (trackingValue.old_value) { - trackingValue.old_value = - moment(trackingValue.old_value) - .local() - .format('LL'); - } - if (trackingValue.new_value) { - trackingValue.new_value = - moment(trackingValue.new_value) - .local() - .format('LL'); - } - } - }); - } - }, - /** - * @private - * @param {Object} data - * @param {string} [data.body = ""] - * @param {(string|integer)[]} [data.channel_ids] - * @param {Object[]} [data.notifications] - * @param {string} [data.email_from] - * @param {string} [data.info] - * @param {string} [data.model] - * @param {string} [data.moderation_status='accepted'] - * @param {string} [data.module_icon] - * @param {Array} [data.needaction_partner_ids = []] - * @param {Array} [data.history_partner_ids = []] - * @param {string} [data.record_name] - * @param {integer} [data.res_id] - * @param {Array} [data.starred_partner_ids = []] - * @param {string} [data.subject] - * @param {string} [data.subtype_description] - * @param {Object[]} [data.tracking_value_ids] - */ - _setInitialData(data) { - this._notifications = data.notifications || {}; - this._documentModel = data.model; - this._documentName = data.record_name; - this._documentID = data.res_id; - this._emailFrom = data.email_from; - this._info = data.info; - this._isNote = data.is_note; - this._moduleIcon = data.module_icon; - this._needactionPartnerIDs = data.needaction_partner_ids || []; - this._starredPartnerIDs = data.starred_partner_ids || []; - this._historyPartnerIDs = data.history_partner_ids || []; - this._subject = data.subject; - this._subtypeDescription = data.subtype_description; - this._threadIDs = data.channel_ids || []; - this._trackingValueIDs = data.tracking_value_ids; - - this._moderationStatus = data.moderation_status || 'accepted'; - }, - /* - * Set whether the message is history or not. - * If it is history, the message is moved to the "History" mailbox. - * Note that this function only applies it locally, the server is not aware - * - * @private - * @param {boolean} history if set, the message is history - */ - _setHistory: function (history) { - if (history) { - this._addThread('mailbox_history'); - } else { - this.removeThread('mailbox_history'); - } - }, - /** - * Set whether the message is moderated by current user or not. - * If it is moderated by the current user, the message is moved to the - * "Moderation" mailbox. Note that this function only applies it locally, - * the server is not aware - * - * @private - * @param {boolean} moderated if set, the message is moderated by user - */ - _setModeratedByUser: function (moderated) { - if (moderated) { - this._addThread('mailbox_moderation'); - } else { - this.removeThread('mailbox_moderation'); - } - }, - /** - * Set whether the message is needaction or not. - * If it is needaction, the message is moved to the "Inbox" mailbox. - * Note that this function only applies it locally, the server is not aware - * - * @private - * @param {boolean} needaction if set, the message is needaction - */ - _setNeedaction: function (needaction) { - if (needaction) { - this._addThread('mailbox_inbox'); - } else { - this.removeThread('mailbox_inbox'); - } - }, - /** - * @private - */ - _warnMessageModerated: function () { - var mailBus = this.call('mail_service', 'getMailBus'); - if (this.needsModerationByUser()) { - this._setModeratedByUser(false); - var moderationBox = this.call('mail_service', 'getMailbox', 'moderation'); - moderationBox.decrementMailboxCounter(); - moderationBox.removeMessage(this.getID()); - mailBus.trigger('update_moderation_counter'); - } - if (this._moderationStatus !== 'accepted') { - this.call('mail_service', 'removeMessageFromThreads', this); - } - mailBus.trigger('update_message', this); - }, - -}); - -return Message; - -}); diff --git a/addons/mail/static/src/js/models/threads/abstract_thread.js b/addons/mail/static/src/js/models/threads/abstract_thread.js deleted file mode 100644 index 619bf95b8e3..00000000000 --- a/addons/mail/static/src/js/models/threads/abstract_thread.js +++ /dev/null @@ -1,231 +0,0 @@ -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 - * (which are compatible with mail service) or website livechats. - * - * Abstract threads contain abstract messages - */ -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; - this._status = params.data.status || ''; - this._unreadCounter = 0; // amount of messages not yet been read - }, - - //-------------------------------------------------------------------------- - // 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 - * - * @param {boolean} folded - */ - fold: function (folded) { - this._folded = folded; - }, - /** - * Get the ID of this thread - * - * @returns {integer|string} - */ - getID: function () { - return this._id; - }, - /** - * @abstract - * @returns {mail.model.AbstractMessage[]} - */ - getMessages: function () {}, - /** - * Get the name of this thread. If the name of the thread has been created - * by the user from an input, it may be escaped. - * - * @returns {string} - */ - getName: function () { - return this._name; - }, - /** - * By default, threads do not have out-of-office feature - * - * @return {string|undefined} - */ - getOutOfOfficeInfo: function () { - return undefined; - }, - /** - * Get the status of the thread (e.g. 'online', 'offline', etc.) - * - * @returns {string} - */ - getStatus: function () { - return this._status; - }, - /** - * Returns the title to display in thread window's headers. - * - * @returns {string} the name of the thread by default (see @getName) - */ - getTitle: function () { - return this.getName(); - }, - getType: function () {}, - /** - * @returns {integer} - */ - getUnreadCounter: function () { - return this._unreadCounter; - }, - /** - * @returns {boolean} - */ - hasMessages: function () { - return !_.isEmpty(this.getMessages()); - }, - /** - * @return {boolean} - */ - hasOutOfOffice() { - return !!this.getOutOfOfficeInfo(); - }, - /** - * States whether this thread is compatible with the 'seen' feature. - * By default, threads do not have thsi feature active. - * @see {mail.model.ThreadSeenMixin} to enable this feature on a thread. - * - * @returns {boolean} - */ - hasSeenFeature: function () { - return false; - }, - /** - * States whether this thread is compatible with the 'is typing...' feature. - * By default, threads do not have this feature active. - * @see {mail.model.ThreadTypingMixin} to enable this feature on a thread. - * - * @returns {boolean} - */ - hasTypingNotification: function () { - return false; - }, - /** - * States whether this thread is folded or not. - * - * @return {boolean} - */ - 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 Promise.resolve(); - }, - /** - * 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; - this._warnUpdatedUnreadCounter(); - }, - - //-------------------------------------------------------------------------- - // 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(); - return Promise.resolve(); - }, - /** - * Post a message on this thread - * - * @abstract - * @private - * @returns {Promise} resolved with the message object to be sent to the - * server - */ - _postMessage: function () { - return Promise.resolve(); - }, - /** - * Warn views (e.g. discuss app, thread window, etc.) to update visually - * the unread counter of this thread. - * - * @abstract - * @private - */ - _warnUpdatedUnreadCounter: function () {}, -}); - -return AbstractThread; - -}); diff --git a/addons/mail/static/src/js/models/threads/channel.js b/addons/mail/static/src/js/models/threads/channel.js deleted file mode 100644 index dced861c00a..00000000000 --- a/addons/mail/static/src/js/models/threads/channel.js +++ /dev/null @@ -1,474 +0,0 @@ -odoo.define('mail.model.Channel', function (require) { -"use strict"; - -var SearchableThread = require('mail.model.SearchableThread'); -var ThreadTypingMixin = require('mail.model.ThreadTypingMixin'); -var mailUtils = require('mail.utils'); - -const config = require('web.config'); -var session = require('web.session'); -var time = require('web.time'); - -/** - * This class represent channels in JS. In this context, the word channel - * has the same meaning of channel on the server, meaning that direct messages - * (DM) and livechats are also channels. - * - * 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 = SearchableThread.extend(ThreadTypingMixin, { - /** - * @override - * @param {Object} params - * @param {Object} params.data - * @param {string} params.data.channel_type - * @param {integer} [params.data.create_uid] the ID of the user that has - * created the channel. - * @param {boolean} params.data.group_based_subscription - * @param {boolean} [params.data.is_minimized=false] - * @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 {Object[]} [params.data.members=[]] - * @param {integer} [params.data.members[i].id] - * @param {string} [params.data.members[i].name] - * @param {string} [params.data.members[i].email] - * @param {integer} [params.data.message_unread_counter] - * @param {boolean} [params.data.moderation=false] whether the channel is - * moderated or not - * @param {Object[]} [params.data.partners_info=[]] - * @param {integer} [params.data.partners_info[i].partner_id] - * @param {integer} [params.data.partners_info[i].fetched_message_id] - * @param {integer} [params.data.partners_info[i].seen_message_id] - * @param {string} params.data.state - * @param {string} [params.data.uuid] - * @param {Object} params.options - * @param {boolean} [params.options.autoswitch=true] - * @param {Object[]} params.commands - */ - init: function (params) { - var self = this; - this._super.apply(this, arguments); - ThreadTypingMixin.init.apply(this, arguments); - - var data = params.data; - var options = params.options; - var commands = params.commands; - - // If set, autoswitch channel on joining this channel in discuss - // the default behaviour is to autoswitch on join. - // exception: receiving channel session notifications - this._autoswitch = 'autoswitch' in options ? options.autoswitch : true; - this._commands = undefined; - this._creatorUID = data.create_uid; - this._detached = data.is_minimized || false; - this._directPartnerID = undefined; - this._folded = data.state === 'folded'; - // if set: hide 'Leave channel' button - this._groupBasedSubscription = data.group_based_subscription; - this._isModerated = data.moderation; - this._isMyselfModerator = data.is_moderator; - this._lastMessageDate = undefined; - this._members = data.members || []; - // Promise that is resolved on fetched members of this channel. - this._membersDef = undefined; - // 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; - this._serverType = data.channel_type; - // unique identifier for this channel, which is required for some rpc - this._uuid = data.uuid; - - // * list of commands available for this channel - this._commands = _.filter(commands, function (command) { - return !command.channel_types || - _.contains(command.channel_types, self._serverType); - }); - if (data.last_message_date) { - this._lastMessageDate = moment(time.str_to_datetime(data.last_message_date)); - } - if (data.message_unread_counter) { - this._unreadCounter = data.message_unread_counter; - } - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Close the channel - * - * This operation is executed server-side and the chat window will be - * folded in all potential tabs from all browsers. - * - * @override - */ - close: function () { - this._super.apply(this, arguments); - // Do not notify the server to avoid desktop chat window from closing - // when a chat window is closed on mobile. - if (!config.device.isMobile) { - this._rpc({ - model: 'mail.channel', - method: 'channel_fold', - kwargs: { uuid: this.getUUID(), state: 'closed' }, - }, { shadow: true }); - } - }, - /** - * Decrement the needaction counter of the channel - * Floor value at 0. - * - * @param {integer} [num=1] the amount to decrement at most - */ - decrementNeedactionCounter: function (num) { - num = _.isNumber(num) ? num : 1; - this._needactionCounter = Math.max(this._needactionCounter - 1, 0); - }, - /** - * Open the chat window for this channel - * (in all potential tabs from all browsers) - * - * @pverride - */ - detach: function () { - var self = this; - return this._super.apply(this, arguments).then(function () { - // Do not notify the server to avoid desktop chat window from opening - // when a chat window is opened on mobile. - if (!config.device.isMobile) { - self._rpc({ - model: 'mail.channel', - method: 'channel_minimize', - args: [self.getUUID(), true], - }, { - shadow: true, - }); - } - }); - }, - /** - * Force fetch members of the channel - * - * @returns {Promise<Object[]>} resolved with list of channel listeners - */ - forceFetchMembers: function () { - this._membersDef = undefined; - return this.getMentionPartnerSuggestions(); - }, - /** - * Folds/Minimize the channel - * (in all potential tabs from all browsers) - * - * @override - * @param {boolean} folded - */ - fold: function (folded) { - this._super.apply(this, arguments); - var args = { - uuid: this.getUUID(), - }; - if (_.isBoolean(folded)) { - args.state = folded ? 'folded' : 'open'; - } - this._rpc({ - model: 'mail.channel', - method: 'channel_fold', - kwargs: args, - }, {shadow: true}); - }, - /** - * Show the list of available commands on this channel (e.g. '/help') - * - * @override - * @returns {Array} list of commands - */ - getCommands: function () { - return this._commands; - }, - /** - * @returns {mail.model.Message|undefined} last message of channel, if any - */ - getLastMessage: function () { - return _.last(this._cache['[]'].messages); - }, - /** - * Get listeners of a channel - * - * @returns {Promise<Object[]>} resolved with list of list of - * channel listeners. - */ - getMentionPartnerSuggestions: function () { - var self = this; - if (!this._membersDef) { - this._membersDef = this._rpc({ - model: 'mail.channel', - method: 'channel_fetch_listeners', - args: [this.getUUID()], - }, { - shadow: true - }) - .then(function (members) { - self._members = members; - return [members]; - }); - } - return this._membersDef; - }, - /** - * @returns {integer} - */ - getNeedactionCounter: function () { - return this._needactionCounter; - }, - /** - * @override - */ - getPreview: function () { - var result = this._super.apply(this, arguments); - if (!this.isTwoUserThread()) { - result.imageSRC = '/web/image/mail.channel/' + this.getID() + '/image_128'; - } - var lastMessage = this.getLastMessage(); - return _.extend(result, { - author: lastMessage ? lastMessage.getDisplayedAuthor() : '', - body: lastMessage ? mailUtils.htmlToTextContentInline(lastMessage.getBody()) : '', - date: lastMessage ? lastMessage.getDate() : undefined, - isMyselfAuthor: this.hasMessages() && this.getLastMessage().isMyselfAuthor(), - }); - }, - /** - * Get the UUID of the channel. - * This is a string that uniquely links this channel to a server channel. - * - * @returns {string} uuid of this channel - */ - getUUID: function () { - return this._uuid; - }, - /** - * Increment the needaction counter of this channel by 1 unit - */ - incrementNeedactionCounter: function () { - this._needactionCounter++; - }, - /** - * States whether the channel should be auto-selected on creation - * - * Note that this is not of the responsibility of the channel - * (see mail.model.Thread for more information) - * - * @override - * @returns {boolean} - */ - isAutoswitch: function () { - return this._autoswitch; - }, - /** - * All instances of this class are chanels - * - * @override - * @returns {boolean} - */ - isChannel: function () { - return true; - }, - /** - * States whether the channel auto-subscribes some users in a group - * - * @returns {boolean} - */ - isGroupBasedSubscription: function () { - return this._groupBasedSubscription; - }, - /** - * States whether the channel is moderated or not. - * - * @override - * @returns {boolean} - */ - isModerated: function () { - 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} - */ - isMyselfModerator: function () { - return this._isMyselfModerator; - }, - /** - * Unsubscribes from channel - * - * @abstract - * @returns {Promise} resolve when unsubscribed - */ - unsubscribe: function () {}, - /** - * Updates the internal state of the channel, and reflects the changes in - * the UI. - * - * Called by {mail.Manager.Notification} on receiving a channel window - * notification. A notification is received after a client-side action - * that changes the state of the channel window visually, so that cross tab - * channels are updated similarly. - * - * TODO: This method should be dropped if the state of windows are - * completely handled client-side, which should be the case in the near - * future. - * - * @param {Object} params - * @param {boolean} params.detached - * @param {boolean} params.folded - */ - updateWindowState: function (params) { - this._folded = 'folded' in params ? params.folded : this._folded; - this._detached = 'detached' in params ? params.detached : this._detached; - this._warnUpdatedWindowState(); - }, - /** - * Reset the needaction counter to 0. - */ - resetNeedactionCounter: function () { - this._needactionCounter = 0; - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Override so that it tells whether the channel is moderated or not. This - * is useful in order to display pending moderation messages when the - * current user is either moderator of the channel or has posted some - * messages that are pending moderation. - * - * @override - * @private - * @returns {Object} - */ - _getFetchMessagesKwargs: function () { - var kwargs = this._super.apply(this, arguments); - if (this.isModerated()) { - kwargs.moderated_channel_ids = [this.getID()]; - } - return kwargs; - }, - /** - * Get the domain to fetch all the messages in the current channel - * - * @override - * @private - * @returns {Array} - */ - _getThreadDomain: function () { - return [['channel_ids', 'in', [this._id]]]; - }, - /** - * @override {mail.model.ThreadTypingMixin} - * @private - * @param {Object} params - * @param {integer} params.partnerID - */ - _isTypingMyselfInfo: function (params) { - return session.partner_id === params.partnerID; - }, - /** - * 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._notifySeen(); - return Promise.all([superDef, seenDef]); - }, - /** - * @override {mail.model.ThreadTypingMixin} - * @private - * @param {Object} params - * @param {boolean} params.typing - * @returns {Promise} - */ - _notifyMyselfTyping: function (params) { - return this._rpc({ - model: 'mail.channel', - method: 'notify_typing', - args: [this.getID()], - kwargs: { is_typing: params.typing }, - }, { shadow: true }); - }, - /** - * @private - * @returns {$.Promise<integer>} resolved with ID of last seen message - */ - _notifySeen: function () { - var self = this; - return this._rpc({ - model: 'mail.channel', - method: 'channel_seen', - args: [[this._id]], - }, { shadow: true }).then(function (lastSeenMessageID) { - self._lastSeenMessageID = lastSeenMessageID; - return lastSeenMessageID; - }); - }, - /** - * 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_xmlid: '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; - }); - }); - }, - /** - * Warn views that the list of users that are currently typing on this - * thread has been updated. - * - * @override {mail.model.ThreadTypingMixin} - * @private - */ - _warnUpdatedTypingPartners: function () { - this.call('mail_service', 'getMailBus') - .trigger('update_typing_partners', this.getID()); - }, -}); - -return Channel; - -}); diff --git a/addons/mail/static/src/js/models/threads/create_mode_document_thread.js b/addons/mail/static/src/js/models/threads/create_mode_document_thread.js deleted file mode 100644 index da632de0470..00000000000 --- a/addons/mail/static/src/js/models/threads/create_mode_document_thread.js +++ /dev/null @@ -1,75 +0,0 @@ -odoo.define('mail.model.CreateModeDocumentThread', function (require) { -"use strict"; - -var AbstractMessage = require('mail.model.AbstractMessage'); -var AbstractThread = require('mail.model.AbstractThread'); - -var core = require('web.core'); -var session = require('web.session'); - -var _t = core._t; - -/** - * Special document thread that is linked to a new document that is in create - * mode. This thread only display a single (fake) message which displays the - * user avatar and the message 'Creating a new record...'. - * - * Note: it uses AbstractThread and AbstractMessage, because this is a fake - * thread which contains a fake message. We do not want to register both of them - * in the mail service. - */ -var CreateModeDocumentThread = AbstractThread.extend({ - - /** - * @override - */ - init: function () { - var params = { - data: { - id: '_createModeDocumentThread' - } - }; - - this._super(params); - - this._messages = this._forgeCreateMessages(); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @returns {mail.model.AbstractMessage[]} - */ - getMessages: function () { - return this._messages; - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Make a fake list of messages to render in create mode. - * Instead of rendering no messages at all, it displays a single message - * with body content "Creating a new record". - * - * @private - * @returns {mail.model.AbstractMessage[]} an array containing a single - * message 'Creating a new record...' - */ - _forgeCreateMessages: function () { - var createMessage = new AbstractMessage(this, { - id: 0, - body: _t("<p>Creating a new record...</p>"), - author_id: [session.partner_id, session.partner_display_name], - }); - return [createMessage]; - }, - -}); - -return CreateModeDocumentThread; - -}); diff --git a/addons/mail/static/src/js/models/threads/dm_chat.js b/addons/mail/static/src/js/models/threads/dm_chat.js deleted file mode 100644 index 177c5224ea4..00000000000 --- a/addons/mail/static/src/js/models/threads/dm_chat.js +++ /dev/null @@ -1,109 +0,0 @@ -odoo.define('mail.model.DMChat', function (require) { -"use strict"; - -var ChannelSeenMixin = require('mail.model.ChannelSeenMixin'); -var TwoUserChannel = require('mail.model.TwoUserChannel'); - -var core = require('web.core'); -const { str_to_datetime } = require('web.time'); - -var _t = core._t; - -/** - * 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 DMChat = TwoUserChannel.extend(ChannelSeenMixin, { - /** - * @override - * @param {Object} params - * @param {Object} params.data - * @param {string|undefined} [params.custom_channel_name] if set, use this - * custom name for this DM - * @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 - * @param {string} [params.data.direct_partner[0].out_of_office_date_end] - */ - init: function (params) { - this._super.apply(this, arguments); - ChannelSeenMixin.init.apply(this, arguments); - - var data = params.data; - - this._directPartnerID = data.direct_partner[0].id; - this._name = data.custom_channel_name || data.direct_partner[0].name; - this._outOfOfficeDateEnd = data.direct_partner[0].out_of_office_date_end; - this._type = 'dm_chat'; - - this.call('mail_service', 'updateImStatus', [{ - id: this._directPartnerID, - im_status: data.direct_partner[0].im_status - }]); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * 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 chats do have a - * direct partner iD. - * - * @returns {integer} - */ - getDirectPartnerID: function () { - return this._directPartnerID; - }, - /** - * Get the out of office info - * - * @override {mail.model.AbstractThread} - * @returns {string|undefined} - */ - getOutOfOfficeInfo: function () { - if (!this._outOfOfficeDateEnd) { - return undefined; - } - var currentDate = new Date(); - var date = str_to_datetime(this._outOfOfficeDateEnd); - var options = { day: 'numeric', month: 'short' }; - if (currentDate.getFullYear() !== date.getFullYear()) { - options.year = 'numeric'; - } - var formattedDate = date.toLocaleDateString(window.navigator.language, options); - return _.str.sprintf(_t("Out of office until %s"), formattedDate); - }, - /** - * @override - */ - getPreview: function () { - var result = this._super.apply(this, arguments); - result.imageSRC = '/web/image/res.partner/' + this.getDirectPartnerID() + '/image_128'; - return result; - }, - /** - * @override - * @return {string} - */ - getStatus: function () { - return this.call('mail_service', 'getImStatus', { partnerID: this._directPartnerID }); - }, - - /** - * @param {Object} data - * @param {string} data.outOfOfficeDateEnd - */ - updateOutOfOfficeInfo: function (data) { - this._outOfOfficeDateEnd = data.outOfOfficeDateEnd; - this.call('mail_service', 'getMailBus').trigger('updated_out_of_office', { - threadID: this.getID(), - }); - } -}); - -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 deleted file mode 100644 index eefc8f9bd79..00000000000 --- a/addons/mail/static/src/js/models/threads/document_thread.js +++ /dev/null @@ -1,364 +0,0 @@ -odoo.define('mail.model.DocumentThread', function (require) { -"use strict"; - -var Thread = require('mail.model.Thread'); - -var session = require('web.session'); - -/** - * These threads represent threads that are directly attached to documents. - * It is sometimes called the "chatter", but technically this is just the - * part of the chatter containing the thread, as the chatter also contains - * the activity and listener fields. - * - * Note that there are still some hacks in the handling of document threads, - * due to document threads are instantiated from fetched messages, either from - * mail.ThreadField or by fetching messages in Inbox. - * - * Also, because messages are not sent on the longpoll bus for this kind of - * threads, it stores the list of message IDs that it contains, which should - * be up-to-date whenever someone wants to get the messages in the document - * thread. - */ -var DocumentThread = Thread.extend({ - - /** - * @override - * @param {Object} params - * @param {Object} params.data - * @param {string} [params.data.messagesIDs] the list of message ids linked - * to the document (if not given, they will be fetched before fetching the - * messages) - * @param {string} params.data.model - * @param {integer} params.data.resID - */ - init: function (params) { - var data = params.data; - data.id = data.resModel + '_' + data.resID; - data.type = 'document_thread'; - - this._super.apply(this, arguments); - - // ID of the related document - this._documentID = data.resID; - // model of the related document - this._documentModel = data.resModel; - // used to handle history of messages - this._messageIDs = data.messageIDs || []; - // list of loaded messages - this._messages = []; - - // if the messageIDs haven't been given, fetch them before fetching the - // messages - this._mustFetchMessageIDs = !_.isArray(data.messageIDs); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Overrides to store the thread's state in the LocalStorage, so that it is - * shared between tabs, and restored on F5. - * - * @override - * @param {Object} [options] - * @param {boolean} [options.skipCrossTabSync=false] if set, it should - * not notify other tabs from updating document thread state. This happens - * in case the `close` operation comes from local storage event. - */ - close: function (options) { - this._super.apply(this, arguments); - if (options && options.skipCrossTabSync) { - return; - } - this.call('mail_service', 'updateDocumentThreadState', this._id, { - name: this.getName(), - windowState: 'closed', - }); - }, - /** - * Overrides to store the thread's state in the LocalStorage, so that it is - * shared between tabs, and restored on F5. - * - * @override - * @param {Object} [options] - * @param {boolean} [options.skipCrossTabSync=false] if set, it should - * not notify other tabs from updating document thread state. This happens - * in case the `detach` operation comes from local storage event. - */ - detach: function (options) { - this._super.apply(this, arguments); - if (options && options.skipCrossTabSync) { - return; - } - var windowState = this._folded ? 'folded' : 'open'; - this.call('mail_service', 'updateDocumentThreadState', this._id, { - name: this.getName(), - windowState: windowState, - }); - }, - /** - * - * Fetch messages of the document thread - * - * @override - * @param {Object} options - * @param {boolean} [options.forceFetch] if true, fetch anyway, as user - * clicked on 'load more'. - * @returns {Promise<mail.model.Message[]>} - */ - fetchMessages: function (options) { - var self = this; - return this._fetchMessages(options).then(function () { - return self._messages; - }); - }, - /** - * Overrides to store the thread's state in the LocalStorage, so that it is - * shared between tabs, and restored on F5. - * - * @override - * @param {Object} [options] - * @param {boolean} [options.skipCrossTabSync=false] if set, it should - * not notify other tabs from updating document thread state. This happens - * in case the `detach` operation comes from local storage event. - */ - fold: function (folded, options) { - this._super.apply(this, arguments); - if (options && options.skipCrossTabSync) { - return; - } - var windowState = this._folded ? 'folded' : 'open'; - this.call('mail_service', 'updateDocumentThreadState', this._id, { - name: this.getName(), - windowState: windowState, - }); - }, - /** - * Get the model name of the document that is linked to this document thread - * - * @returns {string} - */ - getDocumentModel: function () { - return this._documentModel; - }, - /** - * Get the ID of the document that is linked to this document thread - * - * @returns {integer} - */ - getDocumentID: function () { - return this._documentID; - }, - /** - * Get he list of message IDs that this document threads contain. - * - * @returns {integer[]} - */ - getMessageIDs: function () { - return this._messageIDs; - }, - /** - * @override - * @returns {mail.model.Message[]} - */ - getMessages: function () { - return this._messages; - }, - /** - * States whether the thread is linked to a document - * Document thread's are always linked to a document. - * - * @override - * @returns {boolean} - */ - isLinkedToDocument: function () { - return true; - }, - /** - * @param {integer[]} attachmentIDs - */ - removeAttachmentsFromMessages: function (attachmentIDs) { - _.each(this.getMessages(), function (message) { - message.removeAttachments(attachmentIDs); - }); - }, - /** - * Set list of message IDs of this document thread - * - * Useful in order to handle message history of the document thread, - * in particular to fetch messages when necessary and/or display 'load more'. - * - * @param {integer[]} messageIDs - */ - setMessageIDs: function (messageIDs) { - this._mustFetchMessageIDs = false; - this._messageIDs = messageIDs; - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Add this message to this document thread. - * - * This is ignored if the message is already linked to this document thread. - * Also, there is no fetch of unread message counter in a document thread, - * as this object is built from messages and/or thread field widget. The - * unread counter of a document thread is the sum of needaction messages - * 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()); - } - // update unread counter - if (message.isNeedaction()) { - this._unreadCounter++; - } - }, - /** - * Get most up to date messageIDs - * - * @private - * @returns {Promise} resolved when message IDs have been fetched and set - * in the model - */ - _fetchMessageIDs: function () { - var self = this; - var resID = this.getDocumentID(); - if (!resID || !this._mustFetchMessageIDs) { - return Promise.resolve(); - } - return this._rpc({ - model: this.getDocumentModel(), - method: 'read', - args: [[resID], ['message_ids']], - }).then(function (result) { - self.setMessageIDs(result[0].message_ids); - }); - }, - /** - * @override - * @private - * @param {Object} options - * @param {boolean} [options.forceFetch] - * @returns {Promise} resolved when messages have been fetched + document - * thread has updated messages - */ - _fetchMessages: function (options) { - var self = this; - return this._fetchMessageIDs().then(function () { - var messageIDs = self._messageIDs; - var loadedMessages = _.filter(self._messages, function (message) { - return _.contains(messageIDs, message.getID()); - }); - var loadedMessageIDs = _.map(loadedMessages, function (message) { - return message.getID(); - }); - - options = options || {}; - var shouldFetch = _.difference( - messageIDs.slice(0, self._FETCH_LIMIT), - loadedMessageIDs - ).length > 0; - if (options.forceFetch || shouldFetch) { - var idsToLoad = _.difference(messageIDs, loadedMessageIDs) - .slice(0, self._FETCH_LIMIT); - return self._rpc({ - model: 'mail.message', - method: 'message_format', - args: [idsToLoad], - context: session.user_context, - }) - .then(function (messagesData) { - _.each(messagesData, function (messageData) { - self.call('mail_service', 'addMessage', messageData, { silent: true }); - }); - }); - } else { - return Promise.resolve(); - } - - }); - }, - /** - * 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_xmlid: data.subtype_xmlid || "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], { - postedFromDocumentThread: true, - }); - 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 deleted file mode 100644 index c3b4a5e2b4d..00000000000 --- a/addons/mail/static/src/js/models/threads/livechat.js +++ /dev/null @@ -1,95 +0,0 @@ -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 - */ - init: function (params) { - this._super.apply(this, arguments); - this._name = params.data.correspondent_name; - this._WEBSITE_USER_NAME = this._name; - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Override so that the list of members has the website use. This is - * necessary in order for the 'is_typing' feature to compute the name to - * display of a user that is typing. - * - * @override - * @returns {Promise<Object[]>} resolved with list of 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]; - }); - }, - /** - * Called when someone starts typing something on the livechat. - * - * Overrides it so that it determine the partner based on the received - * userID. The reason is that anonymous users have the partner ID of the - * admin, which is likely also an operator, so userID must be used in order - * to distinct them. - * - * @override {mail.model.ThreadTypingMixin} - * @param {Object} params - * @param {integer} params.partnerID ID of the partner that is currently - * typing something on the thread. - * @param {boolean} [params.isWebsiteUser=false] whether the typing partner - * is an anonymous user (reminder: they share partnerID with admin). - */ - registerTyping: function (params) { - params.partnerID = this._WEBSITE_USER_ID; - this._super.call(this, params); - }, - /** - * Called when someone stops typing something on the livechat. - * - * Overrides it so that it determine the partner based on the received - * userID. The reason is that anonymous users have the partner ID of the - * admin, which is likely also an operator, so userID must be used in order - * to distinct them. - * - * @override {mail.model.ThreadTypingMixin} - * @param {Object} params - * @param {integer} params.partnerID ID of the partner that stops typing - * something on the thread. - * @param {boolean} [params.isWebsiteUser=false] whether the typing partner - * is an anonymous user (reminder: they share partnerID with admin). - */ - unregisterTyping: function (params) { - params.partnerID = this._WEBSITE_USER_ID; - this._super.call(this, params); - }, -}); - -return Livechat; - -}); diff --git a/addons/mail/static/src/js/models/threads/mailbox.js b/addons/mail/static/src/js/models/threads/mailbox.js deleted file mode 100644 index a08f92f0ab8..00000000000 --- a/addons/mail/static/src/js/models/threads/mailbox.js +++ /dev/null @@ -1,224 +0,0 @@ -odoo.define('mail.model.Mailbox', function (require) { -"use strict"; - -var SearchableThread = require('mail.model.SearchableThread'); - -var core = require('web.core'); - -var _t = core._t; - -/** - * Mailboxes, called "static channels" in earlier version of Odoo, are threads - * that are not represented on the server. We can see them as "threads that are - * not conversations". For instance, the well-known "Inbox" folder contains a - * list of messages, but the inbox does not represent a conversation: Inbox is - * modeled as a mailbox. - */ -var Mailbox = SearchableThread.extend({ - - /** - * @override - * @param {Object} params - * @param {Object} params.data - * @param {string} params.data.id the ID of the mailbox, without the ``mailbox_`` prefix. - * @param {integer} [params.data.mailboxCounter=0] the initial mailbox counter of this mailbox. - */ - init: function (params) { - var data = params.data; - // ID or mailboxes are always prefixed with 'mailbox_' - data.id = 'mailbox_' + data.id; - data.type = 'mailbox'; - this._super.apply(this, arguments); - this._mailboxCounter = data.mailboxCounter || 0; - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Decrement the counter of this mailbox. It floors to 0. - * - * @param {integer} [num=1] how many, at most, the counter is decremented - */ - decrementMailboxCounter: function (num) { - num = _.isNumber(num) ? num : 1; - this._mailboxCounter = Math.max(this._mailboxCounter - num, 0); - }, - /** - * Get the mailbox counter of this mailbox. - * - * @returns {integer} - */ - getMailboxCounter: function () { - return this._mailboxCounter; - }, - /** - * Get the preview of the messages from the inbox mailbox. - * If there are multiple messages associated to a similar document or - * channel, only keep the lastest message of this document/channel in the - * previews. - * - * @returns {Promise<Object[]>} - */ - getMessagePreviews: function () { - var self = this; - - return this.fetchMessages().then(function (messages) { - // pick only last message of chatter - // items = list of objects - // { - // unreadCounter: {integer}, - // message: {mail.model.Message}, - // messageIDs: {integer[]}, - // } - var items = []; - _.each(messages, function (message) { - var unreadCounter = 1; - var messageIDs = [message.getID()]; - var similarItem = _.find(items, function (item) { - return self._areMessagesFromSameDocumentThread(item.message, message) || - self._areMessagesFromSameChannel(item.message, message); - }); - if (similarItem) { - unreadCounter = similarItem.unreadCounter + 1; - messageIDs = similarItem.messageIDs.concat(messageIDs); - var index = _.findIndex(items, similarItem); - items[index] = { - unreadCounter: unreadCounter, - message: message, - messageIDs: messageIDs - }; - } else { - items.push({ - unreadCounter: unreadCounter, - message: message, - messageIDs: messageIDs, - }); - } - }); - return _.map(items, function (item) { - return _.extend(item.message.getPreview(), { - unreadCounter: item.unreadCounter, - messageIDs: item.messageIDs, - }); - }); - }); - }, - /** - * Increment the counter of this mailbox - * - * @param {integer} [num=1] how many, the counter is incremented - */ - incrementMailboxCounter: function (num) { - num = _.isNumber(num) ? num : 1; - this._mailboxCounter = Math.max(this._mailboxCounter + num, 0); - }, - /** - * Marks all messages from the mailbox as read. At the moment, this method - * makes only sense for 'Inbox'. - * - * @param {Array} domain - * @return {Promise} resolved when all messages have been marked as read - * on the server - */ - markAllMessagesAsRead: function (domain) { - if (this._id === 'mailbox_inbox' && this.getMailboxCounter() > 0) { - return this._rpc({ - model: 'mail.message', - method: 'mark_all_as_read', - kwargs: { - domain: domain, - }, - }); - } - return Promise.resolve(); - }, - /** - * @param {integer} newCounter - */ - setMailboxCounter(newCounter) { - this._mailboxCounter = newCounter; - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Tell whether the two messages originate from the same channel. - * If any one of them does not originate from a channel, returns false. - * - * @private - * @param {mail.model.Message} message1 - * @param {mail.model.Message} message2 - * @returns {boolean} - */ - _areMessagesFromSameChannel: function (message1, message2) { - return message1.originatesFromChannel() && - message2.originatesFromChannel() && - message1.getOriginChannelID() === message2.getOriginChannelID(); - }, - /** - * Tell whether the two messages are linked to the same document thread. - * If any one of them is not linked to a document, returns false. - * - * @private - * @param {mail.model.Message} message1 - * @param {mail.model.Message} message2 - * @returns {boolean} - */ - _areMessagesFromSameDocumentThread: function (message1, message2) { - return message1.isLinkedToDocumentThread() && - message2.isLinkedToDocumentThread() && - message1.getDocumentModel() === message2.getDocumentModel() && - message1.getDocumentID() === message2.getDocumentID(); - }, - /** - * Get the domain to fetch all the messages in the current mailbox - * - * @override - * @private - * @returns {Array} - * @throws on missing domain for the provided mailbox (mailboxes should - * always have a domain on messages to fetch) - */ - _getThreadDomain: function () { - if (this._id === 'mailbox_inbox') { - return [['needaction', '=', true]]; - } else if (this._id === 'mailbox_starred') { - return [['starred', '=', true]]; - } else if (this._id === 'mailbox_history') { - return [['needaction', '=', false]]; - } else if (this._id === 'mailbox_moderation') { - return [['need_moderation', '=', true]]; - } else { - throw (_.str(_t("Missing domain for mailbox with ID '%s'"), this._id)); - } - }, - /** - * Post a message from inbox. This is used when using the 'reply' feature - * on a message that is linked to a document thread. - * - * @override - * @private - * @param {Object} messageData - * @param {Object} options - * @param {integer} options.documentID - * @param {string} options.documentModel - * @returns {$.Promise} - */ - _postMessage: function (messageData, options) { - var documentThread = this.call( - 'mail_service', - 'getDocumentThread', - options.documentModel, - options.documentID - ); - return documentThread.postMessage(messageData); - }, -}); - -return Mailbox; - -}); diff --git a/addons/mail/static/src/js/models/threads/mixins/channel_seen_mixin.js b/addons/mail/static/src/js/models/threads/mixins/channel_seen_mixin.js deleted file mode 100644 index 20a5bba5407..00000000000 --- a/addons/mail/static/src/js/models/threads/mixins/channel_seen_mixin.js +++ /dev/null @@ -1,229 +0,0 @@ -odoo.define('mail.model.ChannelSeenMixin', function (require) { -"use strict"; - -var session = require('web.session'); - -/** - * Mixin for enabling the "seen" feature on channels. - */ -var ChannelSeenMixin = { - /** - * Initialize the internal data for seen feature on channels. - * - * Also listens on some internal events of the channel: - * - * - 'message_added': used to notify channel has been fetched - * - * @param {Object} params - * @param {Object[]} [params.data.partners_info=[]] - * @param {integer} [params.data.seen_partners_info[i].partner_id] - * @param {integer|boolean} [params.data.seen_partners_info[i].fetched_message_id=false] - * @param {integer|boolean} [params.data.seen_partners_info[i].seen_message_id=false] - */ - init: function (params) { - this._seenPartnersInfo = params.data.seen_partners_info || []; - - this.on('message_added', this, this._onSeenMessageAdded); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Get list of members that have fetched the provided message on this - * channel, but not yet seen it. Ignore current user in this list. - * - * @param {mail.model.Message} message - * @returns {Object[]} list of members - */ - getFetchedNotSeenMembers: function (message) { - var otherPartnersInfo = _.filter(this._seenPartnersInfo, function (info) { - return info.partner_id !== session.partner_id; - }); - - // seen partner IDs - var seenPartnersInfo = _.filter(otherPartnersInfo, function (info) { - return info.seen_message_id && info.seen_message_id >= message.getID(); - }); - var seenPartnerIDs = _.map(seenPartnersInfo, function (info) { - return info.partner_id; - }); - - // fetched partner IDs (ignore seen) - var fetchedPartnersInfo = _.filter(otherPartnersInfo, function (info) { - var lastFetchedID = info.fetched_message_id || info.seen_message_id || 0; - return lastFetchedID >= message.getID(); - }); - var fetchedPartnerIDs = _.map(fetchedPartnersInfo, function (info) { - return info.partner_id; - }); - fetchedPartnerIDs = _.reject(fetchedPartnerIDs, function (fetchedPartnerID) { - return _.contains(seenPartnerIDs, fetchedPartnerID); - }); - - // fetched members - var fetchedMembers = _.filter(this._members, function (member) { - return _.contains(fetchedPartnerIDs, member.id); - }); - - return fetchedMembers; - }, - /** - * @returns {integer} - */ - getLastMessageIDSeenByEveryone: function () { - var otherPartnersInfo = _.filter(this._seenPartnersInfo, function (info) { - return info.partner_id !== session.partner_id; - }); - var seenMessageIDs = _.map(otherPartnersInfo, function (info) { - return info.seen_message_id || 0; - }); - if (_.isEmpty(seenMessageIDs)) { - return 0; - } - return _.min(seenMessageIDs); - }, - /** - * Get list of members that have seen the provided message on this channel. - * Ignore current user in this list. - * - * @param {mail.model.Message} message - * @returns {Object[]} list of members - */ - getSeenMembers: function (message) { - var otherPartnersInfo = _.filter(this._seenPartnersInfo, function (info) { - return info.partner_id !== session.partner_id; - }); - - // seen partner IDs - var seenPartnersInfo = _.filter(otherPartnersInfo, function (info) { - return info.seen_message_id >= message.getID(); - }); - var seenPartnerIDs = _.map(seenPartnersInfo, function (info) { - return info.partner_id; - }); - - // seen members - var seenMembers = _.filter(this._members, function (member) { - return _.contains(seenPartnerIDs, member.id); - }); - - return seenMembers; - }, - /** - * @param {mail.model.Message} message - * @returns {boolean} - */ - hasEveryoneFetched: function (message) { - var otherPartnersInfo = _.filter(this._seenPartnersInfo, function (info) { - return info.partner_id !== session.partner_id; - }); - var lastFetchedMessageIDs = _.map(otherPartnersInfo, function (info) { - return info.fetched_message_id || info.seen_message_id || 0; - }); - if (_.isEmpty(lastFetchedMessageIDs)) { - return 0; - } - return message.getID() <= _.min(lastFetchedMessageIDs); - }, - /** - * @param {mail.model.Message} message - * @returns {boolean} - */ - hasEveryoneSeen: function (message) { - var otherPartnersInfo = _.filter(this._seenPartnersInfo, function (info) { - return info.partner_id !== session.partner_id; - }); - var lastSeenMessageIDs = _.map(otherPartnersInfo, function (info) { - return info.seen_message_id || 0; - }); - if (_.isEmpty(lastSeenMessageIDs)) { - return 0; - } - return message.getID() <= _.min(lastSeenMessageIDs); - }, - /** - * @param {mail.model.Message} message - * @returns {boolean} - */ - hasSomeoneFetched: function (message) { - var otherPartnersInfo = _.filter(this._seenPartnersInfo, function (info) { - return info.partner_id !== session.partner_id; - }); - var lastFetchedMessageIDs = _.map(otherPartnersInfo, function (info) { - return info.fetched_message_id || info.seen_message_id || 0; - }); - if (_.isEmpty(lastFetchedMessageIDs)) { - return false; - } - var fetched = _.find(lastFetchedMessageIDs, function (msgID) { - return msgID >= message.getID(); - }); - return fetched; - }, - /** - * @param {mail.model.Message} message - * @returns {boolean} - */ - hasSomeoneSeen: function (message) { - var otherPartnersInfo = _.filter(this._seenPartnersInfo, function (info) { - return info.partner_id !== session.partner_id; - }); - var lastSeenMessageIDs = _.map(otherPartnersInfo, function (info) { - return info.seen_message_id || 0; - }); - if (_.isEmpty(lastSeenMessageIDs)) { - return false; - } - var seen = _.find(lastSeenMessageIDs, function (msgID) { - return msgID >= message.getID(); - }); - return seen; - }, - /** - * @returns {boolean} - */ - hasSeenFeature: function () { - return true; - }, - /** - * @param {Object} data - * @param {string} data.info either 'channel_seen' or 'channel_fetched' - * @param {integer} data.partner_id - * @param {integer} data.last_message_id - */ - updateSeenPartnersInfo: function (data) { - var seenInfo = _.findWhere(this._seenPartnersInfo, { partner_id: data.partner_id }); - if (!seenInfo) { - seenInfo = { - partner_id: data.partner_id, - seen_message_id: 0, - fetched_message_id: 0, - }; - this._seenPartnersInfo.push(seenInfo); - } - if (data.info === 'channel_fetched') { - seenInfo.fetched_message_id = data.last_message_id; - } else if (data.info === 'channel_seen') { - seenInfo.fetched_message_id = data.last_message_id; - seenInfo.seen_message_id = data.last_message_id; - } - this.call('mail_service', 'getMailBus').trigger('update_channel', this.getID()); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onSeenMessageAdded: function () { - this.call('mail_service', 'notifyChannelFetched', { channelID: this._id }); - }, -}; - -return ChannelSeenMixin; - -}); diff --git a/addons/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js b/addons/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js deleted file mode 100644 index 3c98836dc70..00000000000 --- a/addons/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js +++ /dev/null @@ -1,360 +0,0 @@ -odoo.define('mail.model.ThreadTypingMixin', function (require) { -"use strict"; - -var CCThrottleFunction = require('mail.model.CCThrottleFunction'); -var Timer = require('mail.model.Timer'); -var Timers = require('mail.model.Timers'); - -var core = require('web.core'); - -var _t = core._t; - -/** - * Mixin for enabling the "is typing..." notification on a type of thread. - */ -var ThreadTypingMixin = { - // Default partner infos - _DEFAULT_TYPING_PARTNER_ID: '_default', - _DEFAULT_TYPING_PARTNER_NAME: 'Someone', - - /** - * Initialize the internal data for typing feature on threads. - * - * Also listens on some internal events of the thread: - * - * - 'message_added': when a message is added, remove the author in the - * typing partners. - * - 'message_posted': when a message is posted, let the user have the - * possibility to immediately notify if he types something right away, - * instead of waiting for a throttle behaviour. - */ - init: function () { - // Store the last "myself typing" status that has been sent to the - // server. This is useful in order to not notify the same typing - // status multiple times. - this._lastNotifiedMyselfTyping = false; - - // Timer of current user that is typing a very long text. When the - // receivers do not receive any typing notification for a long time, - // they assume that the related partner is no longer typing - // something (e.g. they have closed the browser tab). - // This is a timer to let others know that we are still typing - // something, so that they do not assume we stopped typing - // something. - this._myselfLongTypingTimer = new Timer({ - duration: 50*1000, - onTimeout: this._onMyselfLongTypingTimeout.bind(this), - }); - - // Timer of current user that was currently typing something, but - // there is no change on the input for several time. This is used - // in order to automatically notify other users that we have stopped - // typing something, due to making no changes on the composer for - // some time. - this._myselfTypingInactivityTimer = new Timer({ - duration: 5*1000, - onTimeout: this._onMyselfTypingInactivityTimeout.bind(this), - }); - - // Timers of users currently typing in the thread. This is useful - // in order to automatically unregister typing users when we do not - // receive any typing notification after a long time. Timers are - // internally indexed by partnerID. The current user is ignored in - // this list of timers. - this._othersTypingTimers = new Timers({ - duration: 60*1000, - onTimeout: this._onOthersTypingTimeout.bind(this), - }); - - // Clearable and cancellable throttled version of the - // `doNotifyMyselfTyping` method. (basically `notifyMyselfTyping` - // with slight pre- and post-processing) - // @see {mail.model.ResetableThrottleFunction} - // This is useful when the user posts a message and types something - // else: he must notify immediately that he is typing something, - // instead of waiting for the throttle internal timer. - this._throttleNotifyMyselfTyping = CCThrottleFunction({ - duration: 2.5*1000, - func: this._onNotifyMyselfTyping.bind(this), - }); - - // This is used to track the order of registered partners typing - // something, in order to display the oldest typing partners. - this._typingPartnerIDs = []; - - this.on('message_added', this, this._onTypingMessageAdded); - this.on('message_posted', this, this._onTypingMessagePosted); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Get the text to display when some partners are typing something on the - * thread: - * - * - single typing partner: - * - * A is typing... - * - * - two typing partners: - * - * A and B are typing... - * - * - three or more typing partners: - * - * A, B and more are typing... - * - * The choice of the members name for display is not random: it displays - * the user that have been typing for the longest time. Also, this function - * is hard-coded to display at most 2 partners. This limitation comes from - * how translation works in Odoo, for which unevaluated string cannot be - * translated. - * - * @returns {string} list of members that are typing something on the thread - * (excluding the current user). - */ - getTypingMembersToText: function () { - var typingPartnerIDs = this._typingPartnerIDs; - var typingMembers = _.filter(this._members, function (member) { - return _.contains(typingPartnerIDs, member.id); - }); - var sortedTypingMembers = _.sortBy(typingMembers, function (member) { - return _.indexOf(typingPartnerIDs, member.id); - }); - var displayableTypingMembers = sortedTypingMembers.slice(0, 3); - - if (displayableTypingMembers.length === 0) { - return ''; - } else if (displayableTypingMembers.length === 1) { - return _.str.sprintf(_t("%s is typing..."), displayableTypingMembers[0].name); - } else if (displayableTypingMembers.length === 2) { - return _.str.sprintf(_t("%s and %s are typing..."), - displayableTypingMembers[0].name, - displayableTypingMembers[1].name); - } else { - return _.str.sprintf(_t("%s, %s and more are typing..."), - displayableTypingMembers[0].name, - displayableTypingMembers[1].name); - } - }, - /** - * Threads with this mixin have the typing notification feature - * - * @returns {boolean} - */ - hasTypingNotification: function () { - return true; - }, - /** - * Tells if someone other than current user is typing something on this - * thread. - * - * @returns {boolean} - */ - isSomeoneTyping: function () { - return !(_.isEmpty(this._typingPartnerIDs)); - }, - /** - * Register someone that is currently typing something in this thread. - * If this is the current user that is typing something, don't do anything - * (we do not have to display anything) - * - * This method is ignored if we try to register the current user. - * - * @param {Object} params - * @param {integer} params.partnerID ID of the partner linked to the user - * currently typing something on the thread. - */ - registerTyping: function (params) { - if (this._isTypingMyselfInfo(params)) { - return; - } - var partnerID = params.partnerID; - this._othersTypingTimers.registerTimer({ - timeoutCallbackArguments: [partnerID], - timerID: partnerID, - }); - if (_.contains(this._typingPartnerIDs, partnerID)) { - return; - } - this._typingPartnerIDs.push(partnerID); - this._warnUpdatedTypingPartners(); - }, - /** - * This method must be called when the user starts or stops typing something - * in the composer of the thread. - * - * @param {Object} params - * @param {boolean} params.typing tell whether the current is typing or not. - */ - setMyselfTyping: function (params) { - var typing = params.typing; - if (this._lastNotifiedMyselfTyping === typing) { - this._throttleNotifyMyselfTyping.cancel(); - } else { - this._throttleNotifyMyselfTyping(params); - } - - if (typing) { - this._myselfTypingInactivityTimer.reset(); - } else { - this._myselfTypingInactivityTimer.clear(); - } - }, - /** - * Unregister someone from currently typing something in this thread. - * - * @param {Object} params - * @param {integer} params.partnerID ID of the partner related to the user - * that is currently typing something - */ - unregisterTyping: function (params) { - var partnerID = params.partnerID; - this._othersTypingTimers.unregisterTimer({ timerID: partnerID }); - if (!_.contains(this._typingPartnerIDs, partnerID)) { - return; - } - this._typingPartnerIDs = _.reject(this._typingPartnerIDs, function (id) { - return id === partnerID; - }); - this._warnUpdatedTypingPartners(); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Tells whether the provided information on a partner is related to the - * current user or not. - * - * @abstract - * @private - * @param {Object} params - * @param {integer} params.partner ID of partner to check - */ - _isTypingMyselfInfo: function (params) { - return false; - }, - /** - * Notify to the server that the current user either starts or stops typing - * something. - * - * @abstract - * @private - * @param {Object} params - * @param {boolean} params.typing whether we are typing something or not - * @returns {Promise} resolved if the server is notified, rejected - * otherwise - */ - _notifyMyselfTyping: function (params) { - return Promise.resolve(); - }, - /** - * Warn views that the list of users that are currently typing on this - * thread has been updated. - * - * @abstract - * @private - */ - _warnUpdatedTypingPartners: function () {}, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when current user is typing something for a long time. In order - * to not let other users assume that we are no longer typing something, we - * must notify again that we are typing something. - * - * @private - */ - _onMyselfLongTypingTimeout: function () { - this._throttleNotifyMyselfTyping.clear(); - this._throttleNotifyMyselfTyping({ typing: true }); - }, - /** - * Called when current user has something typed in the composer, but is - * inactive for some time. In this case, he automatically notifies that he - * is no longer typing something - * - * @private - */ - _onMyselfTypingInactivityTimeout: function () { - this._throttleNotifyMyselfTyping.clear(); - this._throttleNotifyMyselfTyping({ typing: false }); - }, - /** - * Called by throttled version of notify myself typing - * - * Notify to the server that the current user either starts or stops typing - * something. Remember last notified stuff from the server, and update - * related typing timers. - * - * @private - * @param {Object} params - * @param {boolean} params.typing whether we are typing something or not. - */ - _onNotifyMyselfTyping: function (params) { - var typing = params.typing; - this._lastNotifiedMyselfTyping = typing; - this._notifyMyselfTyping(params); - if (typing) { - this._myselfLongTypingTimer.reset(); - } else { - this._myselfLongTypingTimer.clear(); - } - }, - /** - * Called when current user do not receive a typing notification of someone - * else typing for a long time. In this case, we assume that this person is - * no longer typing something. - * - * @private - * @param {integer} partnerID partnerID of the person we assume he is no - * longer typing something. - */ - _onOthersTypingTimeout: function (partnerID) { - this.unregisterTyping({ partnerID: partnerID }); - }, - /** - * Called when a new message is added to the thread - * On receiving a message from a typing partner, unregister this partner - * from typing partners (otherwise, it will still display it until timeout). - * - * @private - * @param {mail.model.AbstractMessage} message - */ - _onTypingMessageAdded: function (message) { - var partnerID = message.hasAuthor() ? - message.getAuthorID() : - this._DEFAULT_TYPING_PARTNER_ID; - this.unregisterTyping({ partnerID: partnerID }); - }, - /** - * Called when current user has posted a message on this thread. - * - * The current user receives the possibility to immediately notify the - * other users if he is typing something else. - * - * Refresh the context for the current user to notify that he starts or - * stops typing something. In other words, when this function is called and - * then the current user types something, it immediately notifies the - * server as if it is the first time he is typing something. - * - * @private - */ - _onTypingMessagePosted: function () { - this._lastNotifiedMyselfTyping = false; - this._throttleNotifyMyselfTyping.clear(); - this._myselfLongTypingTimer.clear(); - this._myselfTypingInactivityTimer.clear(); - }, -}; - -return ThreadTypingMixin; - -}); 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 deleted file mode 100644 index 3dab9337502..00000000000 --- a/addons/mail/static/src/js/models/threads/multi_user_channel.js +++ /dev/null @@ -1,73 +0,0 @@ -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 = 'multi_user_channel'; - this._public = data.public !== 'private'; - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Returns the title to display in thread window's headers. - * For channels, the title is prefixed with "#". - * - * @override - * @returns {string|Object} the name of the thread by default (see getName) - */ - getTitle: function () { - return "#" + this._super.apply(this, arguments); - }, - /** - * 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; - }, - /** - * @returns {boolean} - */ - isPublic: function () { - return this._public; - }, - /** - * 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/searchable_thread.js b/addons/mail/static/src/js/models/threads/searchable_thread.js deleted file mode 100644 index c2a48a2d410..00000000000 --- a/addons/mail/static/src/js/models/threads/searchable_thread.js +++ /dev/null @@ -1,244 +0,0 @@ -odoo.define('mail.model.SearchableThread', function (require) { -"use strict"; - -var Thread = require('mail.model.Thread'); - -var session = require('web.session'); - -/** - * This is a version of a thread that is handling a cache. - * Basically, this cache is an object indexed by stringified indexes. - * These indexes represent the domain of messages that match this domain. - * - * 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 SearchableThread = Thread.extend({ - - /** - * @override - * @param {Object} params - * @param {Object} params.data - * @param {integer} [params.data.seen_message_id=undefined] - */ - init: function (params) { - this._super.apply(this, arguments); - - this._cache = { - '[]': { - allHistoryLoaded: false, - loaded: false, - messages: [], - } - }; - this._lastSeenMessageID = params.data.seen_message_id; - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - * @param {Array} [domain] - * @param {boolean} [loadMore=false] - */ - fetchMessages: function (domain, loadMore) { - if (loadMore) { - return this._fetchMessages(domain, true); - } - var cache = this._getCache(domain); - if (cache.loaded) { - return Promise.all(cache.messages); - } else { - return this._fetchMessages(domain); - } - }, - /** - * Get the last seen message for a given thread - * - * @return {integer|null} last seen message ID (if any) - */ - getLastSeenMessageID: function () { - return this._lastSeenMessageID || null; - }, - /** - * @override - * @param {Object} [options={}] - * @param {Array} [options.domain=[]] - * @return {mail.model.Message[]} - */ - getMessages: function (options) { - options = options || {}; - var domain = options.domain || []; - return this._getCache(domain).messages; - }, - /** - * Override so that a thread can tell whether there are message based on - * the domain. - * - * @override - * @param {Object} [options={}] - * @param {Array} [options.domain=[]] - * @returns {boolean} - */ - hasMessages: function (options) { - options = options || {}; - var domain = options.domain || []; - return !_.isEmpty(this.getMessages({ domain: domain })); - }, - /** - * Invalidate the caches of the thread - */ - invalidateCaches: function () { - this._cache = _.pick(this._cache, '[]'); - }, - /** - * State whether all messages have been loaded or not - * - * @param {Array} domain - * @return {boolean} - */ - isAllHistoryLoaded: function (domain) { - return this._getCache(domain).allHistoryLoaded; - }, - /** - * Remove message with ID `messageID` from this thread - * - * This is equivalent to removing this message from all the caches - * - * @param {integer} messageID - */ - removeMessage: function (messageID) { - _.each(this._cache, function (cache) { - cache.messages = _.reject(cache.messages, function (message) { - return message.getID() === messageID; - }); - }); - }, - /** - * Set 'messageID' as the ID of the last message seen in this thread - * - * @param {integer} messageID - */ - setLastSeenMessageID: function (messageID) { - this._lastSeenMessageID = messageID; - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Add the message to this thread - * - * @override - * @private - * @param {mail.model.Message} message - * @param {Object} [options={}] - * @param {Array} [options.domain=[]] - * @param {boolean} [options.incrementUnread=false] - */ - _addMessage: function (message, options) { - options = options || {}; - this._super.apply(this, arguments); - var cache = this._getCache(options.domain || []); - var index = _.sortedIndex(cache.messages, message, function (msg) { - return msg.getID(); - }); - if (cache.messages[index] !== message) { - cache.messages.splice(index, 0, message); - } - if ( - !message.isMyselfAuthor() && - options.incrementUnread && - message.getType() !== 'notification' - ) { - this._incrementUnreadCounter(); - } - }, - /** - * Gets messages from thread - * - * @override - * @private - * @param {Array} [pDomain] filter on the messages of the channel - * @param {boolean} [loadMore] Whether it should load more message - * @return {Promise<mail.model.Message[]>} resolved with list of messages - */ - _fetchMessages: function (pDomain, loadMore) { - var self = this; - var domain = this._getThreadDomain(); - var cache = this._getCache(pDomain); - if (pDomain) { - domain = domain.concat(pDomain || []); - } - if (loadMore) { - var minMessageID = cache.messages[0].getID(); - domain = [['id', '<', minMessageID]].concat(domain); - } - return this._rpc({ - model: 'mail.message', - method: 'message_fetch', - args: [domain], - kwargs: this._getFetchMessagesKwargs(), - }).then(function (messages) { - if (!cache.allHistoryLoaded) { - cache.allHistoryLoaded = messages.length < self._FETCH_LIMIT; - } - cache.loaded = true; - _.each(messages, function (message) { - self.call('mail_service', 'addMessage', message, { - silent: true, - domain: pDomain, - }); - }); - cache = self._getCache(pDomain || []); - return cache.messages; - }); - }, - /** - * Get thread content from the cache - * Useful to get cached messages. - * - * @private - * @param {Array} domain - * @return {Object|undefined} - */ - _getCache: function (domain) { - var stringifiedDomain = JSON.stringify(domain || []); - if (!this._cache[stringifiedDomain]) { - this._cache[stringifiedDomain] = { - allHistoryLoaded: false, - loaded: false, - messages: [], - }; - } - return this._cache[stringifiedDomain]; - }, - /** - * Get the kwargs that are passed to the server with the 'message_fetch' - * RPC. - * - * @private - * @returns {Object} - */ - _getFetchMessagesKwargs: function () { - return { - limit: this._FETCH_LIMIT, - context: session.user_context - }; - }, - /** - * Get the domain to fetch all the messages in the current thread - * - * @abstract - * @private - * @returns {Array} - */ - _getThreadDomain: function () {}, -}); - -return SearchableThread; - -}); diff --git a/addons/mail/static/src/js/models/threads/thread.js b/addons/mail/static/src/js/models/threads/thread.js deleted file mode 100644 index 1e99ee84c11..00000000000 --- a/addons/mail/static/src/js/models/threads/thread.js +++ /dev/null @@ -1,383 +0,0 @@ -odoo.define('mail.model.Thread', function (require) { -"use strict"; - -var emojis = require('mail.emojis'); -var AbstractThread = require('mail.model.AbstractThread'); -var mailUtils = require('mail.utils'); - -var ServicesMixin = require('web.ServicesMixin'); - -/** - * This is the super class modeling threads in JS. - * Such threads are stored in the mail manager, and any piece of JS code whose - * logic relies on threads must ideally interact with such objects. - * - * In particular, channels and mailboxes are two different kinds of threads. - */ -var Thread = AbstractThread.extend(ServicesMixin, { - // max number of fetched messages from the server - _FETCH_LIMIT: 30, - /** - * @override - * @param {Object} params - * @param {mail.Manager} params.parent - * @param {Object} params.data - * @param {string} [params.data.channel_type] - * @param {string} params.data.name - * @param {string} [params.data.type] - */ - init: function (params) { - this._super.apply(this, arguments); - // threads are not detached by default - this._detached = false; - // on 1st request to getPreview, fetch data if incomplete. Otherwise it - // means that there is no message in this channel. - this._previewed = false; - this._type = params.data.type || params.data.channel_type; - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Updates the _detached state of the thread. Must be overridden to reflect - * the new state in the interface. - */ - close: function () { - this._detached = false; - this._folded = false; - this._warnUpdatedWindowState(); - }, - /** - * Updates the _detached state of the thread. Must be overridden to reflect - * the new state in the interface. - * - * @param {Object} [options={}] - * @param {boolean} [options.keepFoldState=false] if set, keep the fold state - * of this thread. Otherwise unfold it while detaching it. - * @param {boolean} [options.passively=false] if set, if the thread window - * will be created passively. - * @returns {Promise} - */ - detach: function (options) { - options = options || {}; - this._detached = true; - this._folded = options.keepFoldState ? this._folded : false; - return this._warnUpdatedWindowState({ - passively: options.passively, - }); - }, - /** - * Fetch the list of messages in this thread. - * By default, a thread has no messages. - * - * Note that this method only returns some messages, as we do not want to - * fetch all messages of a thread at once, just to read the last message. - * - * As a result, this method fetches only some messages of the thread, - * starting from the last message in the thread. At most, it fetches - * `this.LIMIT` number of messages at a time. - * - * @abstract - * @returns {Promise<mail.model.Message[]>} - */ - fetchMessages: function () { - return Promise.resolve([]); - }, - /** - * Updates the folded state of the thread. Must be overridden to reflect - * the new state in the interface. - * - * @override - * @param {boolean} folded - */ - fold: function (folded) { - this._super.apply(this, arguments); - this._detached = true; // auto-detach the thread - this._warnUpdatedWindowState(); - }, - /** - * Get the list of available commands for the thread. - * By default, threads do not have any available command. - * - * @returns {Array} - */ - getCommands: function () { - return []; - }, - /** - * Get the listeners of the thread. - * By default, a thread has not listener. - * - * @abstract - * @returns {Promise<Array<Object[]>>} - */ - getMentionPartnerSuggestions: function () { - return Promise.resolve([]); - }, - /** - * Returns the information required to render the preview of this channel. - * - * @returns {Object} a valid object for the rendering of previews - * (@see mail.Preview template) - */ - getPreview: function () { - return { - id: this.getID(), - imageSRC: '/mail/static/src/img/smiley/avatar.jpg', - isTwoUserThread: this.isTwoUserThread(), - status: this.getStatus(), - title: this.getName(), - unreadCounter: this.getUnreadCounter(), - }; - }, - /** - * @returns {string} - */ - getType: function () { - return this._type; - }, - /** - * State whether this channel has been previewed - * - * A channel that has been previewed means that it had the necessary data - * to display its preview format. A channel needs its meta data and the - * last message in order to build its preview format. - * - * This is useful in order to not fetch preview info on this channel more - * than once on channels that have no message at all. - * - * Any received message updates the last_message, so a channel should - * always have all the necessary information to display its preview after - * the 1st time. - * - * @returns {boolean} - */ - hasBeenPreviewed: function () { - return this._previewed; - }, - /** - * State whether there are unread messages in this thread - * - * @returns {boolean} - */ - hasUnreadMessages: function () { - return this._unreadCounter !== 0; - }, - /** - * Increments the needaction counter of this thread. - * - * @abstract - */ - incrementNeedactionCounter: function () {}, - /** - * States whether the thread should be auto-selected on creation - * - * Note that this is not of the responsibility of the thread: it only - * provides guidance for the object that uses threads (e.g. mail.Discuss - * must listen on threads and auto-select the thread if autoswitch is set). - * - * By default, threads are not in autoswitch mode. - * - * @returns {boolean} - */ - isAutoswitch: function () { - return false; - }, - /** - * States whether this thread is a channel or not. A thread is a channel if - * it is an instance of mail.model.Channel (direct or indirect). - * By default, any thread is not a channel - * - * @returns {boolean} - */ - isChannel: function () { - return false; - }, - /** - * States whether this thread is detached or not. - * A thread that is detached must have a thread window linked to itself. - * - * @return {boolean} - */ - isDetached: function () { - return this._detached; - }, - /** - * States whether the thread is linked to a document - * By default, threads are not linked to a document. - * - * @returns {boolean} - */ - isLinkedToDocument: function () { - return false; - }, - /** - * 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). - * - * By default, thread have not mass-mailings feature set. - * - * @return {boolean} - */ - isMassMailing: function () { - return false; - }, - /** - - * States whether the thread is moderated or not. - * By default, threads are not moderated. - * - * @returns {boolean} - */ - isModerated: function () { - return false; - }, - /** - * States whether the current user is moderator of this thread. - * By default, the current user is not moderator of this thread. - * - * @returns {boolean} - */ - 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; - }, - /** - * Mark this channel as previewed - * - * This is useful in order to not fetch preview info on this channel - * is the server has no preview in the first place. - * - * Note: preview fetch is useful only when the channel contains messages - * that have not been fetched at all. After that, this channel instance - * is updated regularly so that the most up-to-date info are available - * to make the preview of this channel. - */ - markAsPreviewed: function () { - this._previewed = true; - }, - /** - * Set the name of this thread - * - * @param {string} newName - */ - setName: function (newName) { - this._name = newName; - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @abstract - * @private - * @return {Promise} - */ - _fetchMessages: function () { - return Promise.resolve(); - }, - /** - * 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) { - // 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, - channel_ids: data.channel_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 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); - }, - /** - * Warn other mail components that the window state of this thread has - * changed (it has become closed, folded, unfolded, detached, etc.) - * - * @private - * @param {Object} [options={}] - * @param {boolean} [options.passively=false] - * @returns {Promise} - */ - _warnUpdatedWindowState: function (options) { - options = options || {}; - return this.call('mail_service', 'updateThreadWindow', this.getID(), options); - }, -}); - -return Thread; - -}); 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 deleted file mode 100644 index 54a4145bd13..00000000000 --- a/addons/mail/static/src/js/models/threads/two_user_channel.js +++ /dev/null @@ -1,32 +0,0 @@ -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; - -}); diff --git a/addons/mail/static/src/js/models/utils/cc_throttle_function.js b/addons/mail/static/src/js/models/utils/cc_throttle_function.js deleted file mode 100644 index b4b87087e6d..00000000000 --- a/addons/mail/static/src/js/models/utils/cc_throttle_function.js +++ /dev/null @@ -1,181 +0,0 @@ -odoo.define('mail.model.CCThrottleFunctionObject', function (require) { -"use strict"; - -var Class = require('web.Class'); - -/** - * This object models the behaviour of the clearable and cancellable (CC) - * throttle version of a provided function. - */ -var CCThrottleFunctionObject = Class.extend({ - - /** - * @param {Object} params - * @param {integer} params.duration duration of the 'cooldown' phase, i.e. - * the minimum duration between the most recent function call that has - * been made and the following function call. - * @param {function} params.func provided function for making the CC - * throttled version. - */ - init: function (params) { - this._arguments = undefined; - this._cooldownTimeout = undefined; - this._duration = params.duration; - this._func = params.func; - this._shouldCallFunctionAfterCD = false; - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Cancel any buffered function call, but keep the cooldown phase running. - */ - cancel: function () { - this._arguments = undefined; - this._shouldCallFunctionAfterCD = false; - }, - /** - * Clear the internal throttle timer, so that the following function call - * is immediate. For instance, if there is a cooldown stage, it is aborted. - */ - clear: function () { - if (this._cooldownTimeout) { - clearTimeout(this._cooldownTimeout); - this._onCooldownTimeout(); - } - }, - /** - * Called when there is a call to the function. This function is throttled, - * so the time it is called depends on whether the "cooldown stage" occurs - * or not: - * - * - no cooldown stage: function is called immediately, and it starts - * the cooldown stage when successful. - * - in cooldown stage: function is called when the cooldown stage has - * ended from timeout. - * - * Note that after the cooldown stage, only the last attempted function - * call will be considered. - */ - do: function () { - this._arguments = Array.prototype.slice.call(arguments); - if (this._cooldownTimeout === undefined) { - this._callFunction(); - } else { - this._shouldCallFunctionAfterCD = true; - } - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Immediately calls the function with arguments of last buffered function - * call. It initiates the cooldown stage after this function call. - * - * @private - */ - _callFunction: function () { - this._func.apply(null, this._arguments); - this._cooldown(); - }, - /** - * Called when the function has been successfully called. The following - * calls to the function with this object should suffer a "cooldown stage", - * which prevents the function from being called until this stage has ended. - * - * @private - */ - _cooldown: function () { - this.cancel(); - this._cooldownTimeout = setTimeout( - this._onCooldownTimeout.bind(this), - this._duration - ); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when the cooldown stage ended from timeout. Calls the function if - * a function call was buffered. - * - * @private - */ - _onCooldownTimeout: function () { - if (this._shouldCallFunctionAfterCD) { - this._callFunction(); - } else { - this._cooldownTimeout = undefined; - } - }, -}); - -return CCThrottleFunctionObject; - -}); - - -odoo.define('mail.model.CCThrottleFunction', function (require) { -"use strict"; - -var CCThrottleFunctionObject = require('mail.model.CCThrottleFunctionObject'); - -/** - * A function that creates a cancellable and clearable (CC) throttle version - * of a provided function. - * - * This throttle mechanism allows calling a function at most once during a - * certain period: - * - * - When a function call is made, it enters a 'cooldown' phase, in which any - * attempt to call the function is buffered until the cooldown phase ends. - * - At most 1 function call can be buffered during the cooldown phase, and the - * latest one in this phase will be considered at its end. - * - When a cooldown phase ends, any buffered function call will be performed - * and another cooldown phase will follow up. - * - * This throttle version has the following interesting properties: - * - * - cancellable: it allows removing a buffered function call during the - * cooldown phase, but it keeps the cooldown phase running. - * - clearable: it allows to clear the internal clock of the throttled function, - * so that any cooldown phase is immediately ending. - * - * @param {Object} params - * @param {integer} params.duration a duration for the throttled behaviour, - * in milli-seconds. - * @param {function} params.func the function to throttle - * @returns {function} the cancellable and clearable throttle version of the - * provided function in argument. - */ -var CCThrottleFunction = function (params) { - var duration = params.duration; - var func = params.func; - - var throttleFunctionObject = new CCThrottleFunctionObject({ - duration: duration, - func: func, - }); - - var callable = function () { - return throttleFunctionObject.do.apply(throttleFunctionObject, arguments); - }; - callable.cancel = function () { - throttleFunctionObject.cancel(); - }; - callable.clear = function () { - throttleFunctionObject.clear(); - }; - - return callable; -}; - -return CCThrottleFunction; - -}); diff --git a/addons/mail/static/src/js/models/utils/timer.js b/addons/mail/static/src/js/models/utils/timer.js deleted file mode 100644 index 9aaae471d2d..00000000000 --- a/addons/mail/static/src/js/models/utils/timer.js +++ /dev/null @@ -1,69 +0,0 @@ -odoo.define('mail.model.Timer', function (require) { -"use strict"; - -var Class = require('web.Class'); - -/** - * This class creates a timer which, when times out, calls a function. - */ -var Timer = Class.extend({ - - /** - * Instantiate a new timer. Note that the timer is not started on - * initialization (@see start method). - * - * @param {Object} params - * @param {number} params.duration duration of timer before timeout in - * milli-seconds. - * @param {function} params.onTimeout function that is called when the - * timer times out. - */ - init: function (params) { - this._duration = params.duration; - this._timeout = undefined; - this._timeoutCallback = params.onTimeout; - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Clears the countdown of the timer. - */ - clear: function () { - clearTimeout(this._timeout); - }, - /** - * Resets the timer, i.e. resets its duration. - */ - reset: function () { - this.clear(); - this.start(); - }, - /** - * Starts the timer, i.e. after a certain duration, it times out and calls - * a function back. - */ - start: function () { - this._timeout = setTimeout(this._onTimeout.bind(this), this._duration); - }, - - //-------------------------------------------------------------------------- - // Handler - //-------------------------------------------------------------------------- - - /** - * Called when the timer times out, calls back a function on timeout. - * - * @private - */ - _onTimeout: function () { - this._timeoutCallback(); - }, - -}); - -return Timer; - -}); diff --git a/addons/mail/static/src/js/models/utils/timers.js b/addons/mail/static/src/js/models/utils/timers.js deleted file mode 100644 index 757945223f7..00000000000 --- a/addons/mail/static/src/js/models/utils/timers.js +++ /dev/null @@ -1,82 +0,0 @@ -odoo.define('mail.model.Timers', function (require) { -"use strict"; - -var Timer = require('mail.model.Timer'); - -var Class = require('web.Class'); - -/** - * This class lists several timers that use a same callback and duration. - */ -var Timers = Class.extend({ - - /** - * Instantiate a new list of timers - * - * @param {Object} params - * @param {integer} params.duration duration of the underlying timers from - * start to timeout, in milli-seconds. - * @param {function} params.onTimeout a function to call back for underlying - * timers on timeout. - */ - init: function (params) { - this._duration = params.duration; - this._timeoutCallback = params.onTimeout; - this._timers = {}; - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Register a timer with ID `timerID` to start. - * - * - an already registered timer with this ID is reset. - * - (optional) can provide a list of arguments that is passed to the - * function callback when timer times out. - * - * @param {Object} params - * @param {Array} [params.timeoutCallbackArguments] - * @param {integer} params.timerID - */ - registerTimer: function (params) { - var timerID = params.timerID; - if (this._timers[timerID]) { - this._timers[timerID].clear(); - } - var timerParams = { - duration: this._duration, - onTimeout: this._timeoutCallback, - }; - if ('timeoutCallbackArguments' in params) { - timerParams.onTimeout = this._timeoutCallback.bind.apply( - this._timeoutCallback, - [null].concat(params.timeoutCallbackArguments) - ); - } else { - timerParams.onTimeout = this._timeoutCallback; - } - this._timers[timerID] = new Timer(timerParams); - this._timers[timerID].start(); - }, - /** - * Unregister a timer with ID `timerID`. The unregistered timer is aborted - * and will not time out. - * - * @param {Object} params - * @param {integer} params.timerID - */ - unregisterTimer: function (params) { - var timerID = params.timerID; - if (this._timers[timerID]) { - this._timers[timerID].clear(); - delete this._timers[timerID]; - } - }, - -}); - -return Timers; - -}); |
