odoo.define('mail/static/src/models/message/message.js', function (require) {
'use strict';
const emojis = require('mail.emojis');
const { registerNewModel } = require('mail/static/src/model/model_core.js');
const { attr, many2many, many2one, one2many } = require('mail/static/src/model/model_field.js');
const { clear } = require('mail/static/src/model/model_field_command.js');
const { addLink, htmlToTextContentInline, parseAndTransform, timeFromNow } = require('mail.utils');
const { str_to_datetime } = require('web.time');
function factory(dependencies) {
class Message extends dependencies['mail.model'] {
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
/**
* @static
* @param {mail.thread} thread
* @param {string} threadStringifiedDomain
*/
static checkAll(thread, threadStringifiedDomain) {
const threadCache = thread.cache(threadStringifiedDomain);
threadCache.update({ checkedMessages: [['link', threadCache.messages]] });
}
/**
* @static
* @param {Object} data
* @return {Object}
*/
static convertData(data) {
const data2 = {};
if ('attachment_ids' in data) {
if (!data.attachment_ids) {
data2.attachments = [['unlink-all']];
} else {
data2.attachments = [
['insert-and-replace', data.attachment_ids.map(attachmentData =>
this.env.models['mail.attachment'].convertData(attachmentData)
)],
];
}
}
if ('author_id' in data) {
if (!data.author_id) {
data2.author = [['unlink-all']];
} else if (data.author_id[0] !== 0) {
// partner id 0 is a hack of message_format to refer to an
// author non-related to a partner. display_name equals
// email_from, so this is omitted due to being redundant.
data2.author = [
['insert', {
display_name: data.author_id[1],
id: data.author_id[0],
}],
];
}
}
if ('body' in data) {
data2.body = data.body;
}
if ('channel_ids' in data && data.channel_ids) {
const channels = data.channel_ids
.map(channelId =>
this.env.models['mail.thread'].findFromIdentifyingData({
id: channelId,
model: 'mail.channel',
})
).filter(channel => !!channel);
data2.serverChannels = [['replace', channels]];
}
if ('date' in data && data.date) {
data2.date = moment(str_to_datetime(data.date));
}
if ('email_from' in data) {
data2.email_from = data.email_from;
}
if ('history_partner_ids' in data) {
data2.isHistory = data.history_partner_ids.includes(this.env.messaging.currentPartner.id);
}
if ('id' in data) {
data2.id = data.id;
}
if ('is_discussion' in data) {
data2.is_discussion = data.is_discussion;
}
if ('is_note' in data) {
data2.is_note = data.is_note;
}
if ('is_notification' in data) {
data2.is_notification = data.is_notification;
}
if ('message_type' in data) {
data2.message_type = data.message_type;
}
if ('model' in data && 'res_id' in data && data.model && data.res_id) {
const originThreadData = {
id: data.res_id,
model: data.model,
};
if ('record_name' in data && data.record_name) {
originThreadData.name = data.record_name;
}
if ('res_model_name' in data && data.res_model_name) {
originThreadData.model_name = data.res_model_name;
}
if ('module_icon' in data) {
originThreadData.moduleIcon = data.module_icon;
}
data2.originThread = [['insert', originThreadData]];
}
if ('moderation_status' in data) {
data2.moderation_status = data.moderation_status;
}
if ('needaction_partner_ids' in data) {
data2.isNeedaction = data.needaction_partner_ids.includes(this.env.messaging.currentPartner.id);
}
if ('notifications' in data) {
data2.notifications = [['insert', data.notifications.map(notificationData =>
this.env.models['mail.notification'].convertData(notificationData)
)]];
}
if ('starred_partner_ids' in data) {
data2.isStarred = data.starred_partner_ids.includes(this.env.messaging.currentPartner.id);
}
if ('subject' in data) {
data2.subject = data.subject;
}
if ('subtype_description' in data) {
data2.subtype_description = data.subtype_description;
}
if ('subtype_id' in data) {
data2.subtype_id = data.subtype_id;
}
if ('tracking_value_ids' in data) {
data2.tracking_value_ids = data.tracking_value_ids;
}
return data2;
}
/**
* Mark all messages of current user with given domain as read.
*
* @static
* @param {Array[]} domain
*/
static async markAllAsRead(domain) {
await this.env.services.rpc({
model: 'mail.message',
method: 'mark_all_as_read',
kwargs: { domain },
});
}
/**
* Mark provided messages as read. Messages that have been marked as
* read are acknowledged by server with response as longpolling
* notification of following format:
*
* [[dbname, 'res.partner', partnerId], { type: 'mark_as_read' }]
*
* @see mail.messaging_notification_handler:_handleNotificationPartnerMarkAsRead()
*
* @static
* @param {mail.message[]} messages
*/
static async markAsRead(messages) {
await this.env.services.rpc({
model: 'mail.message',
method: 'set_message_done',
args: [messages.map(message => message.id)]
});
}
/**
* Applies the moderation `decision` on the provided messages.
*
* @static
* @param {mail.message[]} messages
* @param {string} decision: 'accept', 'allow', ban', 'discard', or 'reject'
* @param {Object|undefined} [kwargs] optional data to pass on
* message moderation. This is provided when rejecting the messages
* for which title and comment give reason(s) for reject.
* @param {string} [kwargs.title]
* @param {string} [kwargs.comment]
*/
static async moderate(messages, decision, kwargs) {
const messageIds = messages.map(message => message.id);
await this.env.services.rpc({
model: 'mail.message',
method: 'moderate',
args: [messageIds, decision],
kwargs: kwargs,
});
}
/**
* Performs the `message_fetch` RPC on `mail.message`.
*
* @static
* @param {Array[]} domain
* @param {integer} [limit]
* @param {integer[]} [moderated_channel_ids]
* @param {Object} [context]
* @returns {mail.message[]}
*/
static async performRpcMessageFetch(domain, limit, moderated_channel_ids, context) {
const messagesData = await this.env.services.rpc({
model: 'mail.message',
method: 'message_fetch',
kwargs: {
context,
domain,
limit,
moderated_channel_ids,
},
}, { shadow: true });
const messages = this.env.models['mail.message'].insert(messagesData.map(
messageData => this.env.models['mail.message'].convertData(messageData)
));
// compute seen indicators (if applicable)
for (const message of messages) {
for (const thread of message.threads) {
if (thread.model !== 'mail.channel' || thread.channel_type === 'channel') {
// disabled on non-channel threads and
// on `channel` channels for performance reasons
continue;
}
this.env.models['mail.message_seen_indicator'].insert({
channelId: thread.id,
messageId: message.id,
});
}
}
return messages;
}
/**
* @static
* @param {mail.thread} thread
* @param {string} threadStringifiedDomain
*/
static uncheckAll(thread, threadStringifiedDomain) {
const threadCache = thread.cache(threadStringifiedDomain);
threadCache.update({ checkedMessages: [['unlink', threadCache.messages]] });
}
/**
* Unstar all starred messages of current user.
*/
static async unstarAll() {
await this.env.services.rpc({
model: 'mail.message',
method: 'unstar_all',
});
}
/**
* @param {mail.thread} thread
* @param {string} threadStringifiedDomain
* @returns {boolean}
*/
isChecked(thread, threadStringifiedDomain) {
// aku todo
const relatedCheckedThreadCache = this.checkedThreadCaches.find(
threadCache => (
threadCache.thread === thread &&
threadCache.stringifiedDomain === threadStringifiedDomain
)
);
return !!relatedCheckedThreadCache;
}
/**
* Mark this message as read, so that it no longer appears in current
* partner Inbox.
*/
async markAsRead() {
await this.async(() => this.env.services.rpc({
model: 'mail.message',
method: 'set_message_done',
args: [[this.id]]
}));
}
/**
* Applies the moderation `decision` on this message.
*
* @param {string} decision: 'accept', 'allow', ban', 'discard', or 'reject'
* @param {Object|undefined} [kwargs] optional data to pass on
* message moderation. This is provided when rejecting the messages
* for which title and comment give reason(s) for reject.
* @param {string} [kwargs.title]
* @param {string} [kwargs.comment]
*/
async moderate(decision, kwargs) {
await this.async(() => this.constructor.moderate([this], decision, kwargs));
}
/**
* Opens the view that allows to resend the message in case of failure.
*/
openResendAction() {
this.env.bus.trigger('do-action', {
action: 'mail.mail_resend_message_action',
options: {
additional_context: {
mail_message_to_resend: this.id,
},
},
});
}
/**
* Refreshes the value of `dateFromNow` field to the "current now".
*/
refreshDateFromNow() {
this.update({ dateFromNow: this._computeDateFromNow() });
}
/**
* Action to initiate reply to current message in Discuss Inbox. Assumes
* that Discuss and Inbox are already opened.
*/
replyTo() {
this.env.messaging.discuss.replyToMessage(this);
}
/**
* Toggle check state of this message in the context of the provided
* thread and its stringifiedDomain.
*
* @param {mail.thread} thread
* @param {string} threadStringifiedDomain
*/
toggleCheck(thread, threadStringifiedDomain) {
const threadCache = thread.cache(threadStringifiedDomain);
if (threadCache.checkedMessages.includes(this)) {
threadCache.update({ checkedMessages: [['unlink', this]] });
} else {
threadCache.update({ checkedMessages: [['link', this]] });
}
}
/**
* Toggle the starred status of the provided message.
*/
async toggleStar() {
await this.async(() => this.env.services.rpc({
model: 'mail.message',
method: 'toggle_message_starred',
args: [[this.id]]
}));
}
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
/**
* @override
*/
static _createRecordLocalId(data) {
return `${this.modelName}_${data.id}`;
}
/**
* @returns {string}
*/
_computeDateFromNow() {
if (!this.date) {
return clear();
}
return timeFromNow(this.date);
}
/**
* @returns {boolean}
*/
_computeFailureNotifications() {
return [['replace', this.notifications.filter(notifications =>
['exception', 'bounce'].includes(notifications.notification_status)
)]];
}
/**
* @private
* @returns {boolean}
*/
_computeHasCheckbox() {
return this.isModeratedByCurrentPartner;
}
/**
* @private
* @returns {boolean}
*/
_computeIsCurrentPartnerAuthor() {
return !!(
this.author &&
this.messagingCurrentPartner &&
this.messagingCurrentPartner === this.author
);
}
/**
* @private
* @returns {boolean}
*/
_computeIsBodyEqualSubtypeDescription() {
if (!this.body || !this.subtype_description) {
return false;
}
const inlineBody = htmlToTextContentInline(this.body);
return inlineBody.toLowerCase() === this.subtype_description.toLowerCase();
}
/**
* The method does not attempt to cover all possible cases of empty
* messages, but mostly those that happen with a standard flow. Indeed
* it is preferable to be defensive and show an empty message sometimes
* instead of hiding a non-empty message.
*
* The main use case for when a message should become empty is for a
* message posted with only an attachment (no body) and then the
* attachment is deleted.
*
* The main use case for being defensive with the check is when
* receiving a message that has no textual content but has other
* meaningful HTML tags (eg. just an ).
*
* @private
* @returns {boolean}
*/
_computeIsEmpty() {
const isBodyEmpty = (
!this.body ||
[
'',
'