mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 14:52:00 +02:00
751 lines
27 KiB
JavaScript
751 lines
27 KiB
JavaScript
/** @odoo-module **/
|
|
|
|
import { registerModel } from '@mail/model/model_core';
|
|
import { attr, many, one } from '@mail/model/model_field';
|
|
import { clear, insert } from '@mail/model/model_field_command';
|
|
import { addLink, htmlToTextContentInline, parseAndTransform } from '@mail/js/utils';
|
|
|
|
import { session } from '@web/session';
|
|
|
|
import { getLangDatetimeFormat, str_to_datetime } from 'web.time';
|
|
|
|
const { markup } = owl;
|
|
|
|
registerModel({
|
|
name: 'Message',
|
|
modelMethods: {
|
|
/**
|
|
* @param {Object} data
|
|
* @return {Object}
|
|
*/
|
|
convertData(data) {
|
|
const data2 = {};
|
|
data2.attachments = data.attachment_ids;
|
|
if ('author' in data) {
|
|
data2.author = data.author;
|
|
}
|
|
if ('body' in data) {
|
|
data2.body = data.body;
|
|
}
|
|
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 ('guestAuthor' in data) {
|
|
data2.guestAuthor = data.guestAuthor;
|
|
}
|
|
if ('history_partner_ids' in data && this.messaging.currentPartner) {
|
|
data2.isHistory = data.history_partner_ids.includes(this.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;
|
|
}
|
|
data2.linkPreviews = data.linkPreviews;
|
|
if ('messageReactionGroups' in data) {
|
|
data2.messageReactionGroups = data.messageReactionGroups;
|
|
}
|
|
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 = originThreadData;
|
|
}
|
|
if ('needaction_partner_ids' in data && this.messaging.currentPartner) {
|
|
data2.isNeedaction = data.needaction_partner_ids.includes(this.messaging.currentPartner.id);
|
|
}
|
|
if ('notifications' in data) {
|
|
data2.notifications = insert(data.notifications.map(notificationData =>
|
|
this.messaging.models['Notification'].convertData(notificationData)
|
|
));
|
|
}
|
|
if ('parentMessage' in data) {
|
|
if (!data.parentMessage) {
|
|
data2.parentMessage = clear();
|
|
} else {
|
|
data2.parentMessage = this.convertData(data.parentMessage);
|
|
}
|
|
}
|
|
if ('recipients' in data) {
|
|
data2.recipients = data.recipients;
|
|
}
|
|
if ('starred_partner_ids' in data && this.messaging.currentPartner) {
|
|
data2.isStarred = data.starred_partner_ids.includes(this.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 ('trackingValues' in data) {
|
|
data2.trackingValues = data.trackingValues;
|
|
}
|
|
|
|
return data2;
|
|
},
|
|
/**
|
|
* Mark all messages of current user with given domain as read.
|
|
*
|
|
* @param {Array[]} domain
|
|
*/
|
|
async markAllAsRead(domain) {
|
|
await this.messaging.rpc({
|
|
model: 'mail.message',
|
|
method: 'mark_all_as_read',
|
|
kwargs: { domain },
|
|
}, { shadow: true });
|
|
},
|
|
/**
|
|
* Mark provided messages as read. Messages that have been marked as
|
|
* read are acknowledged by server with response as bus.
|
|
* notification of following format:
|
|
*
|
|
* [[dbname, 'res.partner', partnerId], { type: 'mark_as_read' }]
|
|
*
|
|
* @see MessagingNotificationHandler:_handleNotificationPartnerMarkAsRead()
|
|
*
|
|
* @param {Message[]} messages
|
|
*/
|
|
async markAsRead(messages) {
|
|
await this.messaging.rpc({
|
|
model: 'mail.message',
|
|
method: 'set_message_done',
|
|
args: [messages.map(message => message.id)]
|
|
});
|
|
},
|
|
/**
|
|
* Performs the given `route` RPC to fetch messages.
|
|
*
|
|
* @param {string} route
|
|
* @param {Object} params
|
|
* @returns {Message[]}
|
|
*/
|
|
async performRpcMessageFetch(route, params) {
|
|
const messagesData = await this.messaging.rpc({ route, params }, { shadow: true });
|
|
if (!this.messaging) {
|
|
return;
|
|
}
|
|
const messages = this.messaging.models['Message'].insert(messagesData.map(
|
|
messageData => this.messaging.models['Message'].convertData(messageData)
|
|
));
|
|
// compute seen indicators (if applicable)
|
|
for (const message of messages) {
|
|
for (const thread of message.threads) {
|
|
if (!thread.channel || thread.channel.channel_type === 'channel') {
|
|
// disabled on non-channel threads and
|
|
// on `channel` channels for performance reasons
|
|
continue;
|
|
}
|
|
this.messaging.models['MessageSeenIndicator'].insert({
|
|
thread,
|
|
message,
|
|
});
|
|
}
|
|
}
|
|
return messages;
|
|
},
|
|
/**
|
|
* Unstar all starred messages of current user.
|
|
*/
|
|
async unstarAll() {
|
|
await this.messaging.rpc({
|
|
model: 'mail.message',
|
|
method: 'unstar_all',
|
|
});
|
|
},
|
|
},
|
|
recordMethods: {
|
|
/**
|
|
* Adds the given reaction on this message.
|
|
*
|
|
* @param {string} content
|
|
*/
|
|
async addReaction(content) {
|
|
const messageData = await this.messaging.rpc({
|
|
route: '/mail/message/add_reaction',
|
|
params: { content, message_id: this.id },
|
|
});
|
|
if (!this.exists()) {
|
|
return;
|
|
}
|
|
this.update(messageData);
|
|
},
|
|
/**
|
|
* Mark this message as read, so that it no longer appears in current
|
|
* partner Inbox.
|
|
*/
|
|
async markAsRead() {
|
|
await this.messaging.rpc({
|
|
model: 'mail.message',
|
|
method: 'set_message_done',
|
|
args: [[this.id]]
|
|
});
|
|
},
|
|
/**
|
|
* Opens the view that allows to resend the message in case of failure.
|
|
*/
|
|
openResendAction() {
|
|
this.env.services.action.doAction(
|
|
'mail.mail_resend_message_action',
|
|
{
|
|
additionalContext: {
|
|
mail_message_to_resend: this.id,
|
|
},
|
|
}
|
|
);
|
|
},
|
|
/**
|
|
* Removes the given reaction from this message.
|
|
*
|
|
* @param {string} content
|
|
*/
|
|
async removeReaction(content) {
|
|
const messageData = await this.messaging.rpc({
|
|
route: '/mail/message/remove_reaction',
|
|
params: { content, message_id: this.id },
|
|
});
|
|
if (!this.exists()) {
|
|
return;
|
|
}
|
|
this.update(messageData);
|
|
},
|
|
/**
|
|
* Toggle the starred status of the provided message.
|
|
*/
|
|
async toggleStar() {
|
|
await this.messaging.rpc({
|
|
model: 'mail.message',
|
|
method: 'toggle_message_starred',
|
|
args: [[this.id]]
|
|
});
|
|
},
|
|
/**
|
|
* Updates the message's content.
|
|
*
|
|
* @param {Object} param0
|
|
* @param {string} param0.body the new body of the message
|
|
* @param {number[]} param0.attachment_ids
|
|
* @param {string[]} param0.attachment_tokens
|
|
*/
|
|
async updateContent({ body, attachment_ids, attachment_tokens }) {
|
|
const messageData = await this.messaging.rpc({
|
|
route: '/mail/message/update_content',
|
|
params: {
|
|
body,
|
|
attachment_ids,
|
|
attachment_tokens,
|
|
message_id: this.id,
|
|
},
|
|
});
|
|
if (!this.messaging) {
|
|
return;
|
|
}
|
|
this.messaging.models['Message'].insert(messageData);
|
|
},
|
|
},
|
|
fields: {
|
|
authorName: attr({
|
|
compute() {
|
|
if (this.author) {
|
|
return this.author.nameOrDisplayName;
|
|
}
|
|
if (this.guestAuthor) {
|
|
return this.guestAuthor.name;
|
|
}
|
|
if (this.email_from) {
|
|
return this.email_from;
|
|
}
|
|
return this.env._t("Anonymous");
|
|
},
|
|
}),
|
|
attachments: many('Attachment', {
|
|
inverse: 'messages',
|
|
}),
|
|
author: one('Partner'),
|
|
avatarUrl: attr({
|
|
compute() {
|
|
if (this.author && (!this.originThread || this.originThread.model !== 'mail.channel')) {
|
|
// TODO FIXME for public user this might not be accessible. task-2223236
|
|
// we should probably use the correspondig attachment id + access token
|
|
// or create a dedicated route to get message image, checking the access right of the message
|
|
return this.author.avatarUrl;
|
|
} else if (this.author && this.originThread && this.originThread.model === 'mail.channel') {
|
|
return `/mail/channel/${this.originThread.id}/partner/${this.author.id}/avatar_128`;
|
|
} else if (this.guestAuthor && (!this.originThread || this.originThread.model !== 'mail.channel')) {
|
|
return this.guestAuthor.avatarUrl;
|
|
} else if (this.guestAuthor && this.originThread && this.originThread.model === 'mail.channel') {
|
|
return `/mail/channel/${this.originThread.id}/guest/${this.guestAuthor.id}/avatar_128?unique=${this.guestAuthor.name}`;
|
|
} else if (this.message_type === 'email') {
|
|
return '/mail/static/src/img/email_icon.png';
|
|
}
|
|
return '/mail/static/src/img/smiley/avatar.jpg';
|
|
},
|
|
}),
|
|
/**
|
|
* This value is meant to be returned by the server
|
|
* (and has been sanitized before stored into db).
|
|
* Do not use this value in a 't-raw' if the message has been created
|
|
* directly from user input and not from server data as it's not escaped.
|
|
*/
|
|
body: attr({
|
|
default: "",
|
|
}),
|
|
/**
|
|
* Whether this message can be deleted.
|
|
*/
|
|
canBeDeleted: attr({
|
|
compute() {
|
|
if (!session.is_admin && !this.isCurrentUserOrGuestAuthor) {
|
|
return false;
|
|
}
|
|
if (!this.originThread) {
|
|
return false;
|
|
}
|
|
if (this.trackingValues.length > 0) {
|
|
return false;
|
|
}
|
|
if (this.message_type !== 'comment') {
|
|
return false;
|
|
}
|
|
if (this.originThread.model === 'mail.channel') {
|
|
return true;
|
|
}
|
|
return this.is_note;
|
|
},
|
|
}),
|
|
/**
|
|
* Whether this message can be starred/unstarred.
|
|
*/
|
|
canStarBeToggled: attr({
|
|
compute() {
|
|
return !this.messaging.isCurrentUserGuest && !this.isTemporary && !this.isTransient;
|
|
},
|
|
}),
|
|
/**
|
|
* Determines the date of the message as a moment object.
|
|
*/
|
|
date: attr(),
|
|
/**
|
|
* States the date of this message as a string (either a relative period
|
|
* in the near past or an actual date for older dates).
|
|
*/
|
|
dateDay: attr({
|
|
compute() {
|
|
if (!this.date) {
|
|
// Without a date, we assume that it's a today message. This is
|
|
// mainly done to avoid flicker inside the UI.
|
|
return this.env._t("Today");
|
|
}
|
|
const date = this.date.format('YYYY-MM-DD');
|
|
if (date === moment().format('YYYY-MM-DD')) {
|
|
return this.env._t("Today");
|
|
} else if (
|
|
date === moment()
|
|
.subtract(1, 'days')
|
|
.format('YYYY-MM-DD')
|
|
) {
|
|
return this.env._t("Yesterday");
|
|
}
|
|
return this.date.format('LL');
|
|
},
|
|
}),
|
|
/**
|
|
* The date time of the message at current user locale time.
|
|
*/
|
|
datetime: attr({
|
|
compute() {
|
|
if (!this.date) {
|
|
return clear();
|
|
}
|
|
return this.date.format(getLangDatetimeFormat());
|
|
},
|
|
}),
|
|
email_from: attr(),
|
|
failureNotifications: many('Notification', {
|
|
compute() {
|
|
return this.notifications.filter(notifications => notifications.isFailure);
|
|
},
|
|
}),
|
|
guestAuthor: one('Guest', {
|
|
inverse: 'authoredMessages',
|
|
}),
|
|
/**
|
|
* States whether the message has some attachments.
|
|
*/
|
|
hasAttachments: attr({
|
|
compute() {
|
|
return this.attachments.length > 0;
|
|
},
|
|
}),
|
|
/**
|
|
* Determines whether the message has a reaction icon.
|
|
*/
|
|
hasReactionIcon: attr({
|
|
compute() {
|
|
return !this.isTemporary && !this.isTransient;
|
|
},
|
|
}),
|
|
id: attr({
|
|
identifying: true,
|
|
}),
|
|
isCurrentUserOrGuestAuthor: attr({
|
|
compute() {
|
|
return !!(
|
|
this.author &&
|
|
this.messaging.currentPartner &&
|
|
this.messaging.currentPartner === this.author
|
|
) || !!(
|
|
this.guestAuthor &&
|
|
this.messaging.currentGuest &&
|
|
this.messaging.currentGuest === this.guestAuthor
|
|
);
|
|
},
|
|
default: false,
|
|
}),
|
|
/**
|
|
* States if the body field is empty, regardless of editor default
|
|
* html content. To determine if a message is fully empty, use
|
|
* `isEmpty`.
|
|
*/
|
|
isBodyEmpty: attr({
|
|
compute() {
|
|
return (
|
|
!this.body ||
|
|
[
|
|
'',
|
|
'<p></p>',
|
|
'<p><br></p>',
|
|
'<p><br/></p>',
|
|
].includes(this.body.replace(/\s/g, ''))
|
|
);
|
|
},
|
|
}),
|
|
/**
|
|
* States whether `body` and `subtype_description` contain similar
|
|
* values.
|
|
*
|
|
* This is necessary to avoid displaying both of them together when they
|
|
* contain duplicate information. This will especially happen with
|
|
* messages that are posted automatically at the creation of a record
|
|
* (messages that serve as tracking messages). They do have hard-coded
|
|
* "record created" body while being assigned a subtype with a
|
|
* description that states the same information.
|
|
*
|
|
* Fixing newer messages is possible by not assigning them a duplicate
|
|
* body content, but the check here is still necessary to handle
|
|
* existing messages.
|
|
*
|
|
* Limitations:
|
|
* - A translated subtype description might not match a non-translatable
|
|
* body created by a user with a different language.
|
|
* - Their content might be mostly but not exactly the same.
|
|
*/
|
|
isBodyEqualSubtypeDescription: attr({
|
|
compute() {
|
|
if (!this.body || !this.subtype_description) {
|
|
return false;
|
|
}
|
|
const inlineBody = htmlToTextContentInline(this.body);
|
|
return inlineBody.toLowerCase() === this.subtype_description.toLowerCase();
|
|
},
|
|
default: false,
|
|
}),
|
|
isDiscussionOrNotification: attr({
|
|
compute() {
|
|
if (this.is_discussion || this.is_notification || this.message_type === "auto_comment") {
|
|
return true;
|
|
}
|
|
return clear();
|
|
},
|
|
default: false,
|
|
}),
|
|
/**
|
|
* Determine whether the message has to be considered empty or not.
|
|
*
|
|
* An empty message has no text, no attachment and no tracking value.
|
|
*/
|
|
isEmpty: attr({
|
|
/**
|
|
* 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 <img/>).
|
|
*/
|
|
compute() {
|
|
return (
|
|
this.isBodyEmpty &&
|
|
!this.hasAttachments &&
|
|
this.trackingValues.length === 0 &&
|
|
!this.subtype_description
|
|
);
|
|
},
|
|
}),
|
|
/**
|
|
* States whether `originThread.name` and `subject` contain similar
|
|
* values except it contains the extra prefix at the start
|
|
* of the subject.
|
|
*
|
|
* This is necessary to avoid displaying the subject, if
|
|
* the subject is same as threadname.
|
|
*/
|
|
isSubjectSimilarToOriginThreadName: attr({
|
|
compute() {
|
|
if (
|
|
!this.subject ||
|
|
!this.originThread ||
|
|
!this.originThread.name
|
|
) {
|
|
return false;
|
|
}
|
|
const threadName = this.originThread.name.toLowerCase().trim();
|
|
const prefixList = ['re:', 'fw:', 'fwd:'];
|
|
let cleanedSubject = this.subject.toLowerCase();
|
|
let wasSubjectCleaned = true;
|
|
while (wasSubjectCleaned) {
|
|
wasSubjectCleaned = false;
|
|
if (threadName === cleanedSubject) {
|
|
return true;
|
|
}
|
|
for (const prefix of prefixList) {
|
|
if (cleanedSubject.startsWith(prefix)) {
|
|
cleanedSubject = cleanedSubject.replace(prefix, '').trim();
|
|
wasSubjectCleaned = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
}),
|
|
isTemporary: attr({
|
|
default: false,
|
|
}),
|
|
isTransient: attr({
|
|
default: false,
|
|
}),
|
|
is_discussion: attr({
|
|
default: false,
|
|
}),
|
|
/**
|
|
* Determine whether the message was a needaction. Useful to make it
|
|
* present in history mailbox.
|
|
*/
|
|
isHistory: attr({
|
|
default: false,
|
|
}),
|
|
/**
|
|
* Determine whether the message is needaction. Useful to make it
|
|
* present in inbox mailbox and messaging menu.
|
|
*/
|
|
isNeedaction: attr({
|
|
default: false,
|
|
}),
|
|
is_note: attr({
|
|
default: false,
|
|
}),
|
|
is_notification: attr({
|
|
default: false,
|
|
}),
|
|
/**
|
|
* Determine whether the current partner is mentioned.
|
|
*/
|
|
isCurrentPartnerMentioned: attr({
|
|
compute() {
|
|
return this.recipients.includes(this.messaging.currentPartner);
|
|
},
|
|
default: false,
|
|
}),
|
|
/**
|
|
* Determine whether the message is highlighted.
|
|
*/
|
|
isHighlighted: attr({
|
|
compute() {
|
|
return (
|
|
this.isCurrentPartnerMentioned &&
|
|
this.originThread &&
|
|
this.originThread.model === 'mail.channel'
|
|
);
|
|
},
|
|
}),
|
|
/**
|
|
* Determine whether the message is starred. Useful to make it present
|
|
* in starred mailbox.
|
|
*/
|
|
isStarred: attr({
|
|
default: false,
|
|
}),
|
|
/**
|
|
* Last tracking value of the message.
|
|
*/
|
|
lastTrackingValue: one('TrackingValue', {
|
|
compute() {
|
|
const {
|
|
length: l,
|
|
[l - 1]: lastTrackingValue,
|
|
} = this.trackingValues;
|
|
if (lastTrackingValue) {
|
|
return lastTrackingValue;
|
|
}
|
|
return clear();
|
|
},
|
|
}),
|
|
linkPreviews: many('LinkPreview', {
|
|
inverse: 'message',
|
|
}),
|
|
/**
|
|
* Groups of reactions per content allowing to know the number of
|
|
* reactions for each.
|
|
*/
|
|
messageReactionGroups: many('MessageReactionGroup', {
|
|
inverse: 'message',
|
|
}),
|
|
messageTypeText: attr({
|
|
compute() {
|
|
if (this.message_type === 'notification') {
|
|
return this.env._t("System notification");
|
|
}
|
|
if (this.message_type === "auto_comment") {
|
|
return this.env._t("Automated message");
|
|
}
|
|
if (!this.is_discussion && !this.is_notification) {
|
|
return this.env._t("Note");
|
|
}
|
|
return this.env._t("Message");
|
|
},
|
|
}),
|
|
message_type: attr(),
|
|
notificationMessageViews: many('NotificationMessageView', {
|
|
inverse: 'message',
|
|
isCausal: true,
|
|
}),
|
|
/**
|
|
* States the views that are displaying this message.
|
|
*/
|
|
messageViews: many('MessageView', {
|
|
inverse: 'message',
|
|
isCausal: true,
|
|
}),
|
|
messageListViewItems: many('MessageListViewItem', {
|
|
inverse: 'message',
|
|
}),
|
|
notifications: many('Notification', {
|
|
inverse: 'message',
|
|
isCausal: true,
|
|
}),
|
|
/**
|
|
* Origin thread of this message (if any).
|
|
*/
|
|
originThread: one('Thread', {
|
|
inverse: 'messagesAsOriginThread',
|
|
}),
|
|
/**
|
|
* States the message that this message replies to (if any). Only makes
|
|
* sense on channels. Other types of threads might have a parent message
|
|
* (parent_id in python) that should be ignored for the purpose of this
|
|
* feature.
|
|
*/
|
|
parentMessage: one('Message'),
|
|
/**
|
|
* This value is meant to be based on field body which is
|
|
* returned by the server (and has been sanitized before stored into db).
|
|
* Do not use this value in a 't-raw' if the message has been created
|
|
* directly from user input and not from server data as it's not escaped.
|
|
*/
|
|
prettyBody: attr({
|
|
/**
|
|
* This value is meant to be based on field body which is
|
|
* returned by the server (and has been sanitized before stored into db).
|
|
* Do not use this value in a 't-raw' if the message has been created
|
|
* directly from user input and not from server data as it's not escaped.
|
|
*/
|
|
compute() {
|
|
if (!this.body) {
|
|
// body null in db, body will be false instead of empty string
|
|
return clear();
|
|
}
|
|
// add anchor tags to urls
|
|
return parseAndTransform(this.body, addLink);
|
|
},
|
|
default: "",
|
|
}),
|
|
prettyBodyAsMarkup: attr({
|
|
compute() {
|
|
return markup(this.prettyBody);
|
|
},
|
|
}),
|
|
recipients: many('Partner'),
|
|
shortTime: attr({
|
|
compute() {
|
|
if (!this.date) {
|
|
return clear();
|
|
}
|
|
return this.date.format('hh:mm');
|
|
},
|
|
}),
|
|
subject: attr(),
|
|
subtype_description: attr(),
|
|
subtype_id: attr(),
|
|
/**
|
|
* All threads that this message is linked to. This field is read-only.
|
|
*/
|
|
threads: many('Thread', {
|
|
compute() {
|
|
const threads = [];
|
|
if (this.isHistory && this.messaging.history) {
|
|
threads.push(this.messaging.history.thread);
|
|
}
|
|
if (this.isNeedaction && this.messaging.inbox) {
|
|
threads.push(this.messaging.inbox.thread);
|
|
}
|
|
if (this.isStarred && this.messaging.starred) {
|
|
threads.push(this.messaging.starred.thread);
|
|
}
|
|
if (this.originThread) {
|
|
threads.push(this.originThread);
|
|
}
|
|
return threads;
|
|
},
|
|
inverse: 'messages',
|
|
}),
|
|
trackingValues: many('TrackingValue', {
|
|
inverse: 'messageOwner',
|
|
isCausal: true,
|
|
sort: [['smaller-first', 'id']],
|
|
}),
|
|
},
|
|
});
|