[REF] mail: move service.update() to discuss models

These `update()` functions are specific to a discuss record,
so it's best to be defined in the model rather than another
service.

Part-of: odoo/odoo#136308
This commit is contained in:
Alexandre Kühn
2023-09-27 13:11:58 +00:00
parent 7422eb643c
commit 603cd70ecc
28 changed files with 555 additions and 582 deletions
@@ -48,6 +48,17 @@ patch(Thread, {
patch(Thread.prototype, {
chatbotScriptId: null,
update(data) {
super.update(...arguments);
if (data.operator_pid) {
this.operator = this._store.Persona.insert({
type: "partner",
id: data.operator_pid[0],
name: data.operator_pid[1],
});
}
},
get isChannel() {
return this.type === "livechat" || super.isChannel;
},
@@ -63,17 +63,6 @@ patch(ThreadService.prototype, {
}
},
async update(thread, data) {
super.update(...arguments);
if (data.operator_pid) {
thread.operator = this.store.Persona.insert({
type: "partner",
id: data.operator_pid[0],
name: data.operator_pid[1],
});
}
},
avatarUrl(author, thread) {
if (thread.type !== "livechat") {
return super.avatarUrl(...arguments);
@@ -28,10 +28,45 @@ export class Attachment extends Record {
}
const attachment = this.get(data) ?? this.new(data);
Object.assign(attachment, { id: data.id });
this.env.services["mail.attachment"].update(attachment, data);
attachment.update(data);
return attachment;
}
update(data) {
assignDefined(this, data, [
"checksum",
"create_date",
"filename",
"mimetype",
"name",
"type",
"url",
"uploading",
"extension",
"accessToken",
"tmpUrl",
"message",
"res_name",
]);
if (!("extension" in data) && data["name"]) {
this.extension = this.name.split(".").pop();
}
if (data.originThread !== undefined) {
const threadData = Array.isArray(data.originThread)
? data.originThread[0][1]
: data.originThread;
this.originThread = this._store.Thread.insert({
model: threadData.model,
id: threadData.id,
});
const thread = this.originThread;
if (this.notIn(thread.attachments)) {
thread.attachments.push(this);
thread.attachments.sort((a1, a2) => (a1.id < a2.id ? 1 : -1));
}
}
}
accessToken;
checksum;
extension;
@@ -19,41 +19,6 @@ export class AttachmentService {
this.rpc = services["rpc"];
}
update(attachment, data) {
assignDefined(attachment, data, [
"checksum",
"create_date",
"filename",
"mimetype",
"name",
"type",
"url",
"uploading",
"extension",
"accessToken",
"tmpUrl",
"message",
"res_name",
]);
if (!("extension" in data) && data["name"]) {
attachment.extension = attachment.name.split(".").pop();
}
if (data.originThread !== undefined) {
const threadData = Array.isArray(data.originThread)
? data.originThread[0][1]
: data.originThread;
attachment.originThread = this.store.Thread.insert({
model: threadData.model,
id: threadData.id,
});
const thread = attachment.originThread;
if (attachment.notIn(thread.attachments)) {
thread.attachments.push(attachment);
thread.attachments.sort((a1, a2) => (a1.id < a2.id ? 1 : -1));
}
}
}
/**
* Remove the given attachment globally.
*
@@ -1,6 +1,7 @@
/* @odoo-module */
import { Record } from "@mail/core/common/record";
import { removeFromArray } from "@mail/utils/common/arrays";
/**
* @class ChannelMember
@@ -28,10 +29,50 @@ export class ChannelMember extends Record {
static insert(data) {
const memberData = Array.isArray(data) ? data[1] : data;
const member = this.get(memberData) ?? this.new(memberData);
this.env.services["discuss.channel.member"].update(member, data);
member.update(data);
return member;
}
update(data) {
const [command, memberData] = Array.isArray(data) ? data : ["insert", data];
this.id = memberData.id;
if ("persona" in memberData) {
this.persona = this._store.Persona.insert({
...(memberData.persona.partner ?? memberData.persona.guest),
type: memberData.persona.guest ? "guest" : "partner",
country: memberData.persona.partner?.country,
channelId: memberData.persona.guest ? memberData.channel.id : null,
});
}
let thread = memberData.thread ?? this.thread;
if (!thread && memberData.channel?.id) {
thread = this._store.Thread.insert({
id: memberData.channel.id,
model: "discuss.channel",
});
}
if (thread && !this.thread) {
this.thread = thread;
}
switch (command) {
case "insert":
{
if (this.thread && this.notIn(this.thread.channelMembers)) {
this.thread.channelMembers.push(this);
}
}
break;
case "unlink":
removeFromArray(this._store.ChannelMember.records, this);
// eslint-disable-next-line no-fallthrough
case "insert-and-unlink":
if (this.thread) {
removeFromArray(this.thread.channelMembers, this);
}
break;
}
}
/** @type {number} */
id;
persona = Record.one("Persona");
@@ -1,7 +1,5 @@
/* @odoo-module */
import { removeFromArray } from "@mail/utils/common/arrays";
import { registry } from "@web/core/registry";
export class ChannelMemberService {
@@ -14,46 +12,6 @@ export class ChannelMemberService {
this.store = services["mail.store"];
}
update(member, data) {
const [command, memberData] = Array.isArray(data) ? data : ["insert", data];
member.id = memberData.id;
if ("persona" in memberData) {
member.persona = this.store.Persona.insert({
...(memberData.persona.partner ?? memberData.persona.guest),
type: memberData.persona.guest ? "guest" : "partner",
country: memberData.persona.partner?.country,
channelId: memberData.persona.guest ? memberData.channel.id : null,
});
}
let thread = memberData.thread ?? member.thread;
if (!thread && memberData.channel?.id) {
thread = this.store.Thread.insert({
id: memberData.channel.id,
model: "discuss.channel",
});
}
if (thread && !member.thread) {
member.thread = thread;
}
switch (command) {
case "insert":
{
if (member.thread && member.notIn(member.thread.channelMembers)) {
member.thread.channelMembers.push(member);
}
}
break;
case "unlink":
removeFromArray(this.store.ChannelMember.records, member);
// eslint-disable-next-line no-fallthrough
case "insert-and-unlink":
if (member.thread) {
removeFromArray(member.thread.channelMembers, member);
}
break;
}
}
/**
* @param {import("models").ChannelMember} member
* @returns {string}
@@ -1,7 +1,9 @@
/* @odoo-module */
import { Record } from "@mail/core/common/record";
import { replaceArrayWithCompare } from "@mail/utils/common/arrays";
import { htmlToTextContentInline } from "@mail/utils/common/format";
import { assignDefined } from "@mail/utils/common/misc";
import { toRaw } from "@odoo/owl";
@@ -36,10 +38,139 @@ export class Message extends Record {
});
}
const message = this.get(data) ?? this.new(data);
this.env.services["mail.message"].update(message, data);
message.update(data);
return message;
}
/** @param {Object} data */
update(data) {
const {
attachment_ids: attachments = this.attachments,
default_subject: defaultSubject = this.defaultSubject,
is_discussion: isDiscussion = this.isDiscussion,
is_note: isNote = this.isNote,
is_transient: isTransient = this.isTransient,
linkPreviews = this.linkPreviews,
message_type: type = this.type,
model: resModel = this.resModel,
module_icon,
notifications = this.notifications,
parentMessage,
recipients = this.recipients,
record_name,
res_id: resId = this.resId,
res_model_name,
subtype_description: subtypeDescription = this.subtypeDescription,
...remainingData
} = data;
assignDefined(this, remainingData);
assignDefined(this, {
defaultSubject,
isDiscussion,
isNote,
isStarred: this._store.user
? this.starred_partner_ids.includes(this._store.user.id)
: false,
isTransient,
parentMessage: parentMessage ? this._store.Message.insert(parentMessage) : undefined,
resId,
resModel,
subtypeDescription,
type,
});
// origin thread before other information (in particular notification insert uses it)
if (this.originThread) {
assignDefined(this.originThread, {
modelName: res_model_name || undefined,
module_icon: module_icon || undefined,
name:
this.originThread.model === "discuss.channel"
? undefined
: record_name || undefined,
});
}
replaceArrayWithCompare(
this.attachments,
attachments.map((attachment) =>
this._store.Attachment.insert({ message: this, ...attachment })
)
);
if (
Array.isArray(this.author) &&
this.author.some((command) => command.includes("clear"))
) {
this.author = undefined;
}
if (data.author?.id) {
this.author = this._store.Persona.insert({
...data.author,
type: "partner",
});
}
if (data.guestAuthor?.id) {
this.author = this._store.Persona.insert({
...data.guestAuthor,
type: "guest",
channelId: this.originThread.id,
});
}
replaceArrayWithCompare(
this.linkPreviews,
linkPreviews.map((data) => this._store.LinkPreview.insert({ ...data, message: this }))
);
replaceArrayWithCompare(
this.notifications,
notifications.map((notification) =>
this._store.Notification.insert({ ...notification, message: this })
)
);
replaceArrayWithCompare(
this.recipients,
recipients.map((recipient) =>
this._store.Persona.insert({ ...recipient, type: "partner" })
)
);
if ("user_follower_id" in data && data.user_follower_id && this._store.self) {
this.originThread.selfFollower = this._store.Follower.insert({
followedThread: this.originThread,
id: data.user_follower_id,
isActive: true,
partner: this._store.self,
});
}
if (data.messageReactionGroups) {
const reactionContentToUnlink = new Set();
const reactionsToInsert = [];
for (const rawReaction of data.messageReactionGroups) {
const [command, reactionData] = Array.isArray(rawReaction)
? rawReaction
: ["insert", rawReaction];
const reaction = this._store.MessageReactions.insert(reactionData);
if (command === "insert") {
reactionsToInsert.push(reaction);
} else {
reactionContentToUnlink.add(reaction.content);
}
}
this.reactions = this.reactions.filter(
({ content }) => !reactionContentToUnlink.has(content)
);
reactionsToInsert.forEach((reaction) => {
const idx = this.reactions.findIndex(({ content }) => reaction.content === content);
if (idx !== -1) {
this.reactions[idx] = reaction;
} else {
this.reactions.push(reaction);
}
});
}
if (this.isNotification && !this.notificationType) {
const parser = new DOMParser();
const htmlBody = parser.parseFromString(this.body, "text/html");
this.notificationType = htmlBody.querySelector(".o_mail_notification")?.dataset.oeType;
}
}
attachments = Record.many("Attachment");
author = Record.one("Persona");
/** @type {string} */
@@ -1,8 +1,7 @@
/* @odoo-module */
import { removeFromArrayWithPredicate, replaceArrayWithCompare } from "@mail/utils/common/arrays";
import { removeFromArrayWithPredicate } from "@mail/utils/common/arrays";
import { convertBrToLineBreak, prettifyMessageContent } from "@mail/utils/common/format";
import { assignDefined } from "@mail/utils/common/misc";
import { markup } from "@odoo/owl";
@@ -202,206 +201,6 @@ export class MessageService {
}
}
/**
* @param {import("models").Message} message
* @param {Object} data
*/
update(message, data) {
const {
attachment_ids: attachments = message.attachments,
default_subject: defaultSubject = message.defaultSubject,
is_discussion: isDiscussion = message.isDiscussion,
is_note: isNote = message.isNote,
is_transient: isTransient = message.isTransient,
linkPreviews = message.linkPreviews,
message_type: type = message.type,
model: resModel = message.resModel,
module_icon,
notifications = message.notifications,
parentMessage,
recipients = message.recipients,
record_name,
res_id: resId = message.resId,
res_model_name,
subtype_description: subtypeDescription = message.subtypeDescription,
...remainingData
} = data;
assignDefined(message, remainingData);
assignDefined(message, {
defaultSubject,
isDiscussion,
isNote,
isStarred: this.store.user
? message.starred_partner_ids.includes(this.store.user.id)
: false,
isTransient,
parentMessage: parentMessage ? this.store.Message.insert(parentMessage) : undefined,
resId,
resModel,
subtypeDescription,
type,
});
// origin thread before other information (in particular notification insert uses it)
if (message.originThread) {
assignDefined(message.originThread, {
modelName: res_model_name || undefined,
module_icon: module_icon || undefined,
name:
message.originThread.model === "discuss.channel"
? undefined
: record_name || undefined,
});
}
replaceArrayWithCompare(
message.attachments,
attachments.map((attachment) =>
this.store.Attachment.insert({ message, ...attachment })
)
);
if (
Array.isArray(message.author) &&
message.author.some((command) => command.includes("clear"))
) {
message.author = undefined;
}
if (data.author?.id) {
message.author = this.store.Persona.insert({
...data.author,
type: "partner",
});
}
if (data.guestAuthor?.id) {
message.author = this.store.Persona.insert({
...data.guestAuthor,
type: "guest",
channelId: message.originThread.id,
});
}
replaceArrayWithCompare(
message.linkPreviews,
linkPreviews.map((data) => this.store.LinkPreview.insert({ ...data, message }))
);
replaceArrayWithCompare(
message.notifications,
notifications.map((notification) =>
this.store.Notification.insert({ ...notification, message })
)
);
replaceArrayWithCompare(
message.recipients,
recipients.map((recipient) =>
this.store.Persona.insert({ ...recipient, type: "partner" })
)
);
if ("user_follower_id" in data && data.user_follower_id && this.store.self) {
message.originThread.selfFollower = this.env.services["mail.store"].Follower.insert({
followedThread: message.originThread,
id: data.user_follower_id,
isActive: true,
partner: this.store.self,
});
}
if (data.messageReactionGroups) {
this._updateReactions(message, data.messageReactionGroups);
}
if (message.isNotification && !message.notificationType) {
const parser = new DOMParser();
const htmlBody = parser.parseFromString(message.body, "text/html");
message.notificationType =
htmlBody.querySelector(".o_mail_notification")?.dataset.oeType;
}
this.env.bus.trigger("mail.message/onUpdate", { message, data });
}
/**
* @param {import("models").Message} message
*/
_updateReactions(message, reactionGroups) {
const reactionContentToUnlink = new Set();
const reactionsToInsert = [];
for (const rawReaction of reactionGroups) {
const [command, reactionData] = Array.isArray(rawReaction)
? rawReaction
: ["insert", rawReaction];
const reaction = this.store.MessageReactions.insert(reactionData);
if (command === "insert") {
reactionsToInsert.push(reaction);
} else {
reactionContentToUnlink.add(reaction.content);
}
}
message.reactions = message.reactions.filter(
({ content }) => !reactionContentToUnlink.has(content)
);
reactionsToInsert.forEach((reaction) => {
const idx = message.reactions.findIndex(({ content }) => reaction.content === content);
if (idx !== -1) {
message.reactions[idx] = reaction;
} else {
message.reactions.push(reaction);
}
});
}
updateNotification(notification, data) {
Object.assign(notification, {
message: data.message,
notification_status: data.notification_status,
notification_type: data.notification_type,
failure_type: data.failure_type,
persona: data.res_partner_id
? this.store.Persona.insert({
id: data.res_partner_id[0],
displayName: data.res_partner_id[1],
type: "partner",
})
: undefined,
});
if (!notification.message.author?.eq(this.store.self)) {
return;
}
const thread = notification.message.originThread;
this.store.NotificationGroup.insert({
modelName: thread?.modelName,
resId: thread?.id,
resModel: thread?.model,
status: notification.notification_status,
type: notification.notification_type,
notifications: [
[notification.isFailure ? "insert" : "insert-and-unlink", notification],
],
});
}
updateNotificationGroup(group, data) {
Object.assign(group, {
modelName: data.modelName ?? group.modelName,
resModel: data.resModel ?? group.resModel,
type: data.type ?? group.type,
status: data.status ?? group.status,
});
const notifications = data.notifications ?? [];
const alreadyKnownNotifications = new Set(group.notifications.map(({ id }) => id));
const notificationIdsToRemove = new Set();
for (const [commandName, notification] of notifications) {
if (commandName === "insert" && !alreadyKnownNotifications.has(notification.id)) {
group.notifications.push(notification);
} else if (commandName === "insert-and-unlink") {
notificationIdsToRemove.add(notification.id);
}
}
group.notifications = group.notifications.filter(
({ id }) => !notificationIdsToRemove.has(id)
);
group.lastMessage = group.notifications[0]?.message;
for (const notification of group.notifications) {
if (group.lastMessage?.id < notification.message?.id) {
group.lastMessage = notification.message;
}
}
group.resIds.add(data.resId);
}
scheduledDateSimple(message) {
return message.scheduledDate.toLocaleString(DateTime.TIME_SIMPLE, {
locale: this.userService.lang?.replace("_", "-"),
@@ -38,13 +38,42 @@ export class NotificationGroup extends Record {
// return reactive
group = this.store.NotificationGroup.records.find((g) => g.eq(group));
}
this.env.services["mail.message"].updateNotificationGroup(group, data);
group.update(data);
if (group.notifications.length === 0) {
removeFromArrayWithPredicate(this.records, (gr) => gr.eq(group));
}
return group;
}
update(data) {
Object.assign(this, {
modelName: data.modelName ?? this.modelName,
resModel: data.resModel ?? this.resModel,
type: data.type ?? this.type,
status: data.status ?? this.status,
});
const notifications = data.notifications ?? [];
const alreadyKnownNotifications = new Set(this.notifications.map(({ id }) => id));
const notificationIdsToRemove = new Set();
for (const [commandName, notification] of notifications) {
if (commandName === "insert" && !alreadyKnownNotifications.has(notification.id)) {
this.notifications.push(notification);
} else if (commandName === "insert-and-unlink") {
notificationIdsToRemove.add(notification.id);
}
}
this.notifications = this.notifications.filter(
({ id }) => !notificationIdsToRemove.has(id)
);
this.lastMessage = this.notifications[0]?.message;
for (const notification of this.notifications) {
if (this.lastMessage?.id < notification.message?.id) {
this.lastMessage = notification.message;
}
}
this.resIds.add(data.resId);
}
notifications = Record.many("Notification");
/** @type {string} */
modelName;
@@ -23,10 +23,38 @@ export class Notification extends Record {
static insert(data) {
const notification = this.get(data) ?? this.new(data);
Object.assign(notification, { id: data.id });
this.env.services["mail.message"].updateNotification(notification, data);
notification.update(data);
return notification;
}
update(data) {
Object.assign(this, {
message: data.message,
notification_status: data.notification_status,
notification_type: data.notification_type,
failure_type: data.failure_type,
persona: data.res_partner_id
? this._store.Persona.insert({
id: data.res_partner_id[0],
displayName: data.res_partner_id[1],
type: "partner",
})
: undefined,
});
if (!this.message.author?.eq(this._store.self)) {
return;
}
const thread = this.message.originThread;
this._store.NotificationGroup.insert({
modelName: thread?.modelName,
resId: thread?.id,
resModel: thread?.model,
status: this.notification_status,
type: this.notification_type,
notifications: [[this.isFailure ? "insert" : "insert-and-unlink", this]],
});
}
/** @type {number} */
id;
message = Record.one("Message");
@@ -1,6 +1,7 @@
/* @odoo-module */
import { AND, Record } from "@mail/core/common/record";
import { assignDefined, nullifyClearCommands } from "@mail/utils/common/misc";
/**
* @typedef {'offline' | 'bot' | 'online' | 'away' | 'im_partner' | undefined} ImStatus
@@ -30,11 +31,24 @@ export class Persona extends Record {
*/
static insert(data) {
const persona = this.get(data) ?? this.new(data);
this.env.services["mail.persona"].update(persona, data);
persona.update(data);
// return reactive version
return persona;
}
update(data) {
nullifyClearCommands(data);
assignDefined(this, { ...data });
if (
this.type === "partner" &&
this.im_status !== "im_partner" &&
!this.is_public &&
!this._store.registeredImStatusPartners?.includes(this.id)
) {
this._store.registeredImStatusPartners?.push(this.id);
}
}
/** @type {number} */
id;
/** @type {boolean | undefined} */
@@ -1,7 +1,5 @@
/* @odoo-module */
import { assignDefined, nullifyClearCommands } from "@mail/utils/common/misc";
import { registry } from "@web/core/registry";
import { useSequential } from "@mail/utils/common/hooks";
import { markRaw } from "@odoo/owl";
@@ -38,19 +36,6 @@ export class PersonaService {
});
}
update(persona, data) {
nullifyClearCommands(data);
assignDefined(persona, { ...data });
if (
persona.type === "partner" &&
persona.im_status !== "im_partner" &&
!persona.is_public &&
!this.store.registeredImStatusPartners?.includes(persona.id)
) {
this.store.registeredImStatusPartners?.push(persona.id);
}
}
async fetchIsCompany(persona) {
if (persona.type !== "partner") {
// non-partner persona are always considered as not a company
@@ -2,7 +2,8 @@
import { AND, Record } from "@mail/core/common/record";
import { ScrollPosition } from "@mail/core/common/scroll_position";
import { onChange } from "@mail/utils/common/misc";
import { replaceArrayWithCompare } from "@mail/utils/common/arrays";
import { assignDefined, nullifyClearCommands, onChange } from "@mail/utils/common/misc";
import { deserializeDateTime } from "@web/core/l10n/dates";
import { _t } from "@web/core/l10n/translation";
@@ -57,7 +58,7 @@ export class Thread extends Record {
}
let thread = this.get(data);
if (thread) {
this.env.services["mail.thread"].update(thread, data);
thread.update(data);
return thread;
}
thread = this.new(data);
@@ -78,12 +79,142 @@ export class Thread extends Record {
this.store.discuss.threadLocalId = null;
}
});
this.env.services["mail.thread"].update(thread, data);
thread.update(data);
this.store.Composer.insert({ thread });
// return reactive version.
return thread;
}
/** @param {Object} data */
update(data) {
const { id, name, attachments: attachmentsData, description, ...serverData } = data;
assignDefined(this, { id, name, description });
if (attachmentsData) {
replaceArrayWithCompare(
this.attachments,
attachmentsData
.map((attachmentData) => this._store.Attachment.insert(attachmentData))
.sort((a1, a2) => a2.id - a1.id)
);
}
if (serverData) {
assignDefined(this, serverData, [
"uuid",
"authorizedGroupFullName",
"description",
"hasWriteAccess",
"is_pinned",
"isLoaded",
"isLoadingAttachments",
"mainAttachment",
"message_unread_counter",
"message_needaction_counter",
"name",
"seen_message_id",
"state",
"type",
"status",
"group_based_subscription",
"last_interest_dt",
"is_editable",
"defaultDisplayMode",
]);
if (serverData.channel && "message_unread_counter" in serverData.channel) {
this.message_unread_counter = serverData.channel.message_unread_counter;
}
const lastServerMessageId = serverData.last_message_id ?? this.lastServerMessage?.id;
if (this.lastServerMessage?.id !== lastServerMessageId) {
this.lastServerMessage = this._store.Message.insert({
id: lastServerMessageId,
});
}
if (this.model === "discuss.channel" && serverData.channel) {
nullifyClearCommands(serverData.channel);
this.channel = assignDefined(this.channel ?? {}, serverData.channel);
}
this.memberCount = serverData.channel?.memberCount ?? this.memberCount;
if (this.type === "chat" && serverData.channel) {
this.customName = serverData.channel.custom_channel_name;
}
if (serverData.channel?.channelMembers) {
for (const [command, membersData] of serverData.channel.channelMembers) {
const members = Array.isArray(membersData) ? membersData : [membersData];
for (const memberData of members) {
const member = this._store.ChannelMember.insert([command, memberData]);
if (this.type !== "chat") {
continue;
}
if (
member.persona.notEq(this._store.user) ||
(serverData.channel.channelMembers[0][1].length === 1 &&
member.persona?.eq(this._store.user))
) {
this.chatPartner = member.persona;
}
}
}
}
if ("invitedMembers" in serverData) {
if (!serverData.invitedMembers) {
this.invitedMemberIds.clear();
return;
}
const command = serverData.invitedMembers[0][0];
const members = serverData.invitedMembers[0][1];
switch (command) {
case "insert":
if (members) {
for (const member of members) {
const record = this._store.ChannelMember.insert(member);
this.invitedMemberIds.add(record.id);
}
}
break;
case "unlink":
case "insert-and-unlink":
// eslint-disable-next-line no-case-declarations
for (const member of members) {
this.invitedMemberIds.delete(member.id);
}
break;
}
}
if ("seen_partners_info" in serverData) {
this.seenInfos = serverData.seen_partners_info.map(
({ fetched_message_id, partner_id, seen_message_id }) => {
return {
lastFetchedMessage: fetched_message_id
? this._store.Message.insert({ id: fetched_message_id })
: undefined,
lastSeenMessage: seen_message_id
? this._store.Message.insert({ id: seen_message_id })
: undefined,
partner: this._store.Persona.insert({
id: partner_id,
type: "partner",
}),
};
}
);
}
}
if (
this.type === "channel" &&
!this._store.discuss.channels.threads.includes(this.localId)
) {
this._store.discuss.channels.threads.push(this.localId);
} else if (
(this.type === "chat" || this.type === "group") &&
!this._store.discuss.chats.threads.includes(this.localId)
) {
this._store.discuss.chats.threads.push(this.localId);
}
if (!this.type && !["mail.box", "discuss.channel"].includes(this.model)) {
this.type = "chatter";
}
}
/** @type {number} */
id;
/** @type {string} */
@@ -2,9 +2,8 @@
import { loadEmoji } from "@web/core/emoji_picker/emoji_picker";
import { DEFAULT_AVATAR } from "@mail/core/common/persona_service";
import { removeFromArray, replaceArrayWithCompare } from "@mail/utils/common/arrays";
import { removeFromArray } from "@mail/utils/common/arrays";
import { prettifyMessageContent } from "@mail/utils/common/format";
import { assignDefined, nullifyClearCommands } from "@mail/utils/common/misc";
import { markup } from "@odoo/owl";
@@ -120,7 +119,7 @@ export class ThreadService {
newUnreadCounter++;
}
}
this.update(thread, {
thread.update({
seen_message_id: lastSeenId,
message_needaction_counter: newNeedactionCounter,
message_unread_counter: newUnreadCounter,
@@ -203,7 +202,7 @@ export class ThreadService {
Object.assign(data, { body: data.body ? markup(data.body) : data.body })
);
});
this.update(thread, { isLoaded: true });
thread.update({ isLoaded: true });
return messages;
} catch (e) {
thread.hasLoadingFailed = true;
@@ -663,140 +662,6 @@ export class ThreadService {
thread.delete();
}
/**
* @param {import("models").Thread} thread
* @param {Object} data
*/
update(thread, data) {
const { id, name, attachments: attachmentsData, description, ...serverData } = data;
assignDefined(thread, { id, name, description });
if (attachmentsData) {
replaceArrayWithCompare(
thread.attachments,
attachmentsData.map((attachmentData) =>
this.store.Attachment.insert(attachmentData)
)
);
}
if (serverData) {
assignDefined(thread, serverData, [
"uuid",
"authorizedGroupFullName",
"description",
"hasWriteAccess",
"is_pinned",
"isLoaded",
"isLoadingAttachments",
"mainAttachment",
"message_unread_counter",
"message_needaction_counter",
"name",
"seen_message_id",
"state",
"type",
"status",
"group_based_subscription",
"last_interest_dt",
"is_editable",
"defaultDisplayMode",
]);
if (serverData.channel && "message_unread_counter" in serverData.channel) {
thread.message_unread_counter = serverData.channel.message_unread_counter;
}
const lastServerMessageId = serverData.last_message_id ?? thread.lastServerMessage?.id;
if (thread.lastServerMessage?.id !== lastServerMessageId) {
thread.lastServerMessage = this.store.Message.insert({
id: lastServerMessageId,
});
}
if (thread.model === "discuss.channel" && serverData.channel) {
nullifyClearCommands(serverData.channel);
thread.channel = assignDefined(thread.channel ?? {}, serverData.channel);
}
thread.memberCount = serverData.channel?.memberCount ?? thread.memberCount;
if (thread.type === "chat" && serverData.channel) {
thread.customName = serverData.channel.custom_channel_name;
}
if (serverData.channel?.channelMembers) {
for (const [command, membersData] of serverData.channel.channelMembers) {
const members = Array.isArray(membersData) ? membersData : [membersData];
for (const memberData of members) {
const member = this.store.ChannelMember.insert([command, memberData]);
if (thread.type !== "chat") {
continue;
}
if (
member.persona.notEq(thread._store.user) ||
(serverData.channel.channelMembers[0][1].length === 1 &&
member.persona?.eq(thread._store.user))
) {
thread.chatPartner = member.persona;
}
}
}
}
if ("invitedMembers" in serverData) {
if (!serverData.invitedMembers) {
thread.invitedMemberIds.clear();
return;
}
const command = serverData.invitedMembers[0][0];
const members = serverData.invitedMembers[0][1];
switch (command) {
case "insert":
if (members) {
for (const member of members) {
const record = this.store.ChannelMember.insert(member);
thread.invitedMemberIds.add(record.id);
}
}
break;
case "unlink":
case "insert-and-unlink":
// eslint-disable-next-line no-case-declarations
for (const member of members) {
thread.invitedMemberIds.delete(member.id);
}
break;
}
}
if ("seen_partners_info" in serverData) {
thread.seenInfos = serverData.seen_partners_info.map(
({ fetched_message_id, partner_id, seen_message_id }) => {
return {
lastFetchedMessage: fetched_message_id
? this.store.Message.insert({ id: fetched_message_id })
: undefined,
lastSeenMessage: seen_message_id
? this.store.Message.insert({ id: seen_message_id })
: undefined,
partner: this.store.Persona.insert({
id: partner_id,
type: "partner",
}),
};
}
);
}
}
if (
thread.type === "channel" &&
!this.store.discuss.channels.threads.includes(thread.localId)
) {
this.store.discuss.channels.threads.push(thread.localId);
} else if (
(thread.type === "chat" || thread.type === "group") &&
!this.store.discuss.chats.threads.includes(thread.localId)
) {
this.store.discuss.chats.threads.push(thread.localId);
}
if (!thread.type && !["mail.box", "discuss.channel"].includes(thread.model)) {
thread.type = "chatter";
}
this.env.bus.trigger("mail.thread/onUpdate", { thread, data });
}
/**
* @param {import("models").Thread} thread
* @param {string} body
@@ -63,7 +63,7 @@ patch(ThreadService.prototype, {
}
}
if ("attachments" in result) {
this.update(thread, {
thread.update({
areAttachmentsLoaded: true,
attachments: result.attachments,
isLoadingAttachments: false,
@@ -141,9 +141,7 @@ export class CallParticipantCard extends Component {
channel_id: this.props.thread.id,
member_ids: [this.channelMember.id],
});
this.threadService.update(this.props.thread, {
invitedMembers: channelData.invitedMembers,
});
this.props.thread.update({ invitedMembers: channelData.invitedMembers });
}
async onClickReplay() {
@@ -2,7 +2,6 @@
import { BlurManager } from "@mail/discuss/call/common/blur_manager";
import { monitorAudio } from "@mail/discuss/call/common/media_monitoring";
import { removeFromArray } from "@mail/utils/common/arrays";
import { closeStream, onChange } from "@mail/utils/common/misc";
import { reactive } from "@odoo/owl";
@@ -158,9 +157,6 @@ export class Rtc {
await this.resetAudioTrack({ force: true });
}
});
this.env.bus.addEventListener("mail.thread/onUpdate", ({ detail: { thread, data } }) => {
this.onThreadUpdate(thread, data);
});
this.env.bus.addEventListener(
"RTC-SERVICE:UPDATE_RTC_SESSIONS",
({ detail: { commands = [], record, thread } }) => {
@@ -284,44 +280,6 @@ export class Rtc {
);
}
onThreadUpdate(thread, data) {
if ("rtc_inviting_session" in data) {
this.env.bus.trigger("RTC-SERVICE:UPDATE_RTC_SESSIONS", {
thread,
record: data.rtc_inviting_session,
});
thread.rtcInvitingSession = this.store.RtcSession.insert({
id: data.rtc_inviting_session.id,
});
if (!this.store.ringingThreads.includes(thread.localId)) {
this.store.ringingThreads.push(thread.localId);
}
}
if ("rtcInvitingSession" in data) {
if (Array.isArray(data.rtcInvitingSession)) {
if (data.rtcInvitingSession[0][0] === "unlink") {
thread.rtcInvitingSession = undefined;
removeFromArray(this.store.ringingThreads, thread.localId);
}
return;
}
this.env.bus.trigger("RTC-SERVICE:UPDATE_RTC_SESSIONS", {
thread,
record: data.rtcInvitingSession,
});
thread.rtcInvitingSession = this.store.RtcSession.insert({
id: data.rtcInvitingSession.id,
});
this.store.ringingThreads.push(thread.localId);
}
if ("rtcSessions" in data) {
this.env.bus.trigger("RTC-SERVICE:UPDATE_RTC_SESSIONS", {
thread,
commands: data.rtcSessions,
});
}
}
/**
* @param {any} id
*/
@@ -781,7 +739,7 @@ export class Rtc {
this.notification.add(_t("Your browser does not support webRTC."), { type: "warning" });
return;
}
const { rtcSessions, iceServers, sessionId, invitedMembers } = await this.rpc(
const { rtcSessions, iceServers, sessionId } = await this.rpc(
"/mail/rtc/channel/join_call",
{
channel_id: channel.id,
@@ -795,7 +753,7 @@ export class Rtc {
this.clear();
this.state.logs.clear();
this.state.channel = channel;
this.onThreadUpdate(this.state.channel, { rtcSessions, invitedMembers });
this.state.channel.update({ rtcSessions });
this.state.selfSession = this.store.RtcSession.get(sessionId);
this.state.iceServers = iceServers || DEFAULT_ICE_SERVERS;
this.state.logs.set("channelId", this.state.channel?.id);
@@ -0,0 +1,47 @@
/* @odoo-module */
import { Thread } from "@mail/core/common/thread_model";
import { removeFromArray } from "@mail/utils/common/arrays";
import { patch } from "@web/core/utils/patch";
patch(Thread.prototype, {
update(data) {
super.update(data);
if ("rtc_inviting_session" in data) {
this.Model.env.bus.trigger("RTC-SERVICE:UPDATE_RTC_SESSIONS", {
thread: this,
record: data.rtc_inviting_session,
});
this.rtcInvitingSession = this._store.RtcSession.insert({
id: data.rtc_inviting_session.id,
});
if (!this._store.ringingThreads.includes(this.localId)) {
this._store.ringingThreads.push(this.localId);
}
}
if ("rtcInvitingSession" in data) {
if (Array.isArray(data.rtcInvitingSession)) {
if (data.rtcInvitingSession[0][0] === "unlink") {
this.rtcInvitingSession = undefined;
removeFromArray(this._store.ringingThreads, this.localId);
}
return;
}
this.Model.env.bus.trigger("RTC-SERVICE:UPDATE_RTC_SESSIONS", {
thread: this,
record: data.rtcInvitingSession,
});
this.rtcInvitingSession = this._store.RtcSession.insert({
id: data.rtcInvitingSession.id,
});
this._store.ringingThreads.push(this.localId);
}
if ("rtcSessions" in data) {
this.Model.env.bus.trigger("RTC-SERVICE:UPDATE_RTC_SESSIONS", {
thread: this,
commands: data.rtcSessions,
});
}
},
});
@@ -49,7 +49,7 @@ export class DiscussCoreCommon {
const { id, last_interest_dt } = payload;
const channel = this.store.Thread.get({ model: "discuss.channel", id });
if (channel) {
this.threadService.update(channel, { last_interest_dt });
channel.update({ last_interest_dt });
if (channel.type !== "channel") {
this.threadService.sortChannels();
}
@@ -29,6 +29,24 @@ patch(MessageModel.prototype, {
/** @type {string} */
this.pinnedAt = undefined;
},
update(data) {
super.update(data);
const { pinned_at: pinnedAt } = data;
if (
this.originThread?.model === "discuss.channel" &&
(pinnedAt !== undefined || this.isEmpty)
) {
if (pinnedAt && !this.isEmpty) {
if (this.notIn(this.originThread.pinnedMessages)) {
this.originThread.pinnedMessages.push(this);
}
this.pinnedAt = pinnedAt;
} else {
delete this.pinnedAt;
this.originThread.pinnedMessages.delete(this);
}
}
},
});
export class MessagePin {
@@ -49,13 +67,6 @@ export class MessagePin {
this.store = services["mail.store"];
}
setup() {
this.env.bus.addEventListener("mail.message/onUpdate", ({ detail: { message, data } }) => {
this.onMessageUpdate(message, data);
});
this.busService.start();
}
/**
* @param {import("models").Thread} channel
*/
@@ -115,27 +126,6 @@ export class MessagePin {
return this.getPinnedMessages(channel).length > 0;
}
/**
* @param {import("models").Message}
* @param {Object} data
*/
onMessageUpdate(message, { pinned_at: pinnedAt }) {
if (
message.originThread?.model === "discuss.channel" &&
(pinnedAt !== undefined || message.isEmpty)
) {
if (pinnedAt && !message.isEmpty) {
if (message.notIn(message.originThread.pinnedMessages)) {
message.originThread.pinnedMessages.push(message);
}
message.pinnedAt = pinnedAt;
} else {
delete message.pinnedAt;
message.originThread.pinnedMessages.delete(message);
}
}
}
/**
* Prompts the user for confirmation, then sets pinned to true.
*
@@ -196,7 +186,6 @@ export const messagePinService = {
*/
start(env, services) {
const messagePin = reactive(new MessagePin(env, services));
messagePin.setup();
return messagePin;
},
};
@@ -4,6 +4,12 @@ import { Attachment } from "@mail/core/common/attachment_model";
import { patch } from "@web/core/utils/patch";
patch(Attachment.prototype, {
update(data) {
super.update(...arguments);
if (data["voice"]) {
this.isVoice = true;
}
},
get isViewable() {
return !this.isVoice && super.isViewable;
},
@@ -9,12 +9,6 @@ patch(AttachmentService.prototype, {
/** @type {import("@mail/discuss/voice_message/common/voice_message_service").VoiceMessageService} */
this.voiceMessageService = services["discuss.voice_message"];
},
update(attachment, data) {
super.update(...arguments);
if (data["voice"]) {
attachment.isVoice = true;
}
},
remove(attachment) {
if (attachment.isVoice && attachment.id > 0) {
this.voiceMessageService.activePlayer = null;
@@ -5,6 +5,22 @@ import { _t } from "@web/core/l10n/translation";
import { patch } from "@web/core/utils/patch";
patch(Persona.prototype, {
update(data) {
super.update(data);
if (this.type === "visitor") {
this.history = data.history ?? this.history;
this.isConnected = data.is_connected ?? this.isConnected;
this.im_status = this.isConnected ? "online" : undefined;
this.langName = data.lang_name ?? this.langName;
this.name = data.display_name ?? this.name;
this.websiteName = data.website_name ?? this.websiteName;
}
if (data.country_id) {
this.country = this.country ?? {};
this.country.id = data.country_id;
this.country.code = data.country_code ?? this.country.code;
}
},
get countryFlagUrl() {
const country = this.partner?.country ?? this.country;
return country
@@ -1,23 +0,0 @@
/** @odoo-module */
import { PersonaService } from "@mail/core/common/persona_service";
import { patch } from "@web/core/utils/patch";
patch(PersonaService.prototype, {
update(persona, data) {
super.update(persona, data);
if (persona.type === "visitor") {
persona.history = data.history ?? persona.history;
persona.isConnected = data.is_connected ?? persona.isConnected;
persona.im_status = persona.isConnected ? "online" : undefined;
persona.langName = data.lang_name ?? persona.langName;
persona.name = data.display_name ?? persona.name;
persona.websiteName = data.website_name ?? persona.websiteName;
}
if (data.country_id) {
persona.country = persona.country ?? {};
persona.country.id = data.country_id;
persona.country.code = data.country_code ?? persona.country.code;
}
},
});
@@ -0,0 +1,18 @@
/** @odoo-module */
import { Thread } from "@mail/core/common/thread_model";
import { assignDefined } from "@mail/utils/common/misc";
import { patch } from "@web/core/utils/patch";
patch(Thread.prototype, {
update(data) {
super.update(data);
if (data?.visitor) {
this.visitor = this._store.Persona.insert({
...data.visitor,
type: "visitor",
});
}
assignDefined(this, data, ["requested_by_operator"]);
},
});
@@ -2,20 +2,9 @@
import { DEFAULT_AVATAR } from "@mail/core/common/persona_service";
import { ThreadService } from "@mail/core/common/thread_service";
import { assignDefined } from "@mail/utils/common/misc";
import { patch } from "@web/core/utils/patch";
patch(ThreadService.prototype, {
update(thread, data) {
super.update(thread, data);
if (data?.visitor) {
thread.visitor = this.store.Persona.insert({
...data.visitor,
type: "visitor",
});
}
assignDefined(thread, data, ["requested_by_operator"]);
},
/**
* @param {import("models").Persona} persona
* @param {import("models").Thread} [thread]
@@ -0,0 +1,12 @@
/** @odoo-module */
import { Thread } from "@mail/core/common/thread_model";
import { assignDefined } from "@mail/utils/common/misc";
import { patch } from "@web/core/utils/patch";
patch(Thread.prototype, {
update(data) {
super.update(data);
assignDefined(this, data, ["requested_by_operator"]);
},
});
@@ -1,12 +0,0 @@
/** @odoo-module */
import { ThreadService } from "@mail/core/common/thread_service";
import { assignDefined } from "@mail/utils/common/misc";
import { patch } from "@web/core/utils/patch";
patch(ThreadService.prototype, {
update(thread, data) {
super.update(thread, data);
assignDefined(thread, data, ["requested_by_operator"]);
},
});