[REF] mail: introduce Record.one() relational field on model.
This allow storing only the local id internally, and there are
automatic `get`/`set` to get the related record. This prepares
support of record deletion that would automatically delete
all relational fields, in a follow-up PR.
```js
class Message {
author = one("Persona");
}
```
Relational fields can be used to uniquely identify records:
```js
class ChatWindow {
static id = "thread";
thread = one("Thread");
}
```
Part-of: odoo/odoo#134884
This commit is contained in:
@@ -28,10 +28,10 @@ threadActionsRegistry.add("open-hr-profile", {
|
||||
const orm = useService("orm");
|
||||
const store = useService("mail.store");
|
||||
let employeeId;
|
||||
if (!component.thread?.correspondent?.employeeId && component.thread?.chatPartnerId) {
|
||||
if (!component.thread?.correspondent?.employeeId && component.thread?.chatPartner) {
|
||||
const employees = await orm.silent.searchRead(
|
||||
"hr.employee",
|
||||
[["user_partner_id", "=", component.thread.chatPartnerId]],
|
||||
[["user_partner_id", "=", component.thread.chatPartner.id]],
|
||||
["id"]
|
||||
);
|
||||
employeeId = employees[0]?.id;
|
||||
|
||||
@@ -39,7 +39,7 @@ export class Attachment extends Record {
|
||||
id;
|
||||
mimetype;
|
||||
name;
|
||||
originThreadLocalId;
|
||||
originThread = Record.one("Thread");
|
||||
res_name;
|
||||
type;
|
||||
/** @type {string} */
|
||||
@@ -48,16 +48,10 @@ export class Attachment extends Record {
|
||||
url;
|
||||
/** @type {boolean} */
|
||||
uploading;
|
||||
/** @type {import("@mail/core/common/message_model").Message} */
|
||||
message;
|
||||
message = Record.one("Message");
|
||||
/** @type {string} */
|
||||
create_date;
|
||||
|
||||
/** @type {import("@mail/core/common/thread_model").Thread} */
|
||||
get originThread() {
|
||||
return this._store.Thread.records[this.originThreadLocalId];
|
||||
}
|
||||
|
||||
get isDeletable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -43,11 +43,10 @@ export class AttachmentService {
|
||||
const threadData = Array.isArray(data.originThread)
|
||||
? data.originThread[0][1]
|
||||
: data.originThread;
|
||||
this.store.Thread.insert({
|
||||
attachment.originThread = this.store.Thread.insert({
|
||||
model: threadData.model,
|
||||
id: threadData.id,
|
||||
});
|
||||
attachment.originThreadLocalId = this.store.Thread.localId(threadData);
|
||||
const thread = attachment.originThread;
|
||||
if (attachment.notIn(thread.attachments)) {
|
||||
thread.attachments.push(attachment);
|
||||
@@ -59,7 +58,7 @@ export class AttachmentService {
|
||||
/**
|
||||
* Remove the given attachment globally.
|
||||
*
|
||||
* @param {Attachment} attachment
|
||||
* @param {import("@mail/core/common/attachment_model").Attachment} attachment
|
||||
*/
|
||||
remove(attachment) {
|
||||
if (attachment.tmpUrl) {
|
||||
|
||||
@@ -6,8 +6,8 @@ import { Record } from "@mail/core/common/record";
|
||||
* @class ChannelMember
|
||||
* @typedef Data
|
||||
* @property {number} id
|
||||
* @property {string} personaLocalId
|
||||
* @property {number} threadId
|
||||
* @property {import("@mail/core/common/persona_model").Persona} persona
|
||||
* @property {import("@mail/core/common/thread_model").Thread} thread
|
||||
*/
|
||||
export class ChannelMember extends Record {
|
||||
static id = "id";
|
||||
@@ -34,25 +34,9 @@ export class ChannelMember extends Record {
|
||||
|
||||
/** @type {number} */
|
||||
id;
|
||||
personaLocalId;
|
||||
rtcSessionId;
|
||||
threadId;
|
||||
|
||||
get persona() {
|
||||
return this._store.Persona.records[this.personaLocalId];
|
||||
}
|
||||
|
||||
set persona(persona) {
|
||||
this.personaLocalId = persona?.localId;
|
||||
}
|
||||
|
||||
get rtcSession() {
|
||||
return this._store.RtcSession.get(this.rtcSessionId);
|
||||
}
|
||||
|
||||
get thread() {
|
||||
return this._store.Thread.get({ model: "discuss.channel", id: this.threadId });
|
||||
}
|
||||
persona = Record.one("Persona");
|
||||
rtcSession = Record.one("RtcSession");
|
||||
thread = Record.one("Thread");
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
|
||||
@@ -25,13 +25,16 @@ export class ChannelMemberService {
|
||||
channelId: memberData.persona.guest ? memberData.channel.id : null,
|
||||
});
|
||||
}
|
||||
member.threadId = memberData.threadId ?? member.threadId ?? memberData.channel?.id;
|
||||
if (member.threadId && !member.thread) {
|
||||
this.store.Thread.insert({
|
||||
id: member.threadId,
|
||||
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":
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div t-if="chatWindowService.hidden.length > 0 and !ui.isSmall" t-ref="hiddenMenu" class="o-mail-ChatWindow-hiddenMenuContainer position-fixed bottom-0">
|
||||
<t t-call="mail.ChatWindowHiddenMenu"/>
|
||||
</div>
|
||||
<t t-foreach="chatWindowService.visible" t-as="chatWindow" t-key="chatWindow.threadLocalId">
|
||||
<t t-foreach="chatWindowService.visible" t-as="chatWindow" t-key="chatWindow.thread?.localId">
|
||||
<ChatWindow chatWindow="chatWindow" right="CHAT_WINDOW_END_GAP_WIDTH + chatWindow_index * (CHAT_WINDOW_WIDTH + CHAT_WINDOW_INBETWEEN_WIDTH * 2)"/>
|
||||
</t>
|
||||
</div>
|
||||
@@ -21,7 +21,7 @@
|
||||
</t>
|
||||
<t t-set-slot="default">
|
||||
<ul class="m-0 p-0 overflow-auto" role="menu" t-ref="list">
|
||||
<li t-foreach="[...chatWindowService.hidden].reverse()" t-as="chatWindow" t-key="chatWindow.threadLocalId" class="o-mail-ChatWindowHiddenMenu-item dropdown-item p-0" t-att-class="{ 'border-bottom' : !chatWindow_last }" role="menuitem">
|
||||
<li t-foreach="[...chatWindowService.hidden].reverse()" t-as="chatWindow" t-key="chatWindow.thread?.localId" class="o-mail-ChatWindowHiddenMenu-item dropdown-item p-0" t-att-class="{ 'border-bottom' : !chatWindow_last }" role="menuitem">
|
||||
<ChatWindow chatWindow="chatWindow"/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { _t } from "@web/core/l10n/translation";
|
||||
/** @typedef {{ thread?: import("@mail/core/common/thread_model").Thread, folded?: boolean, replaceNewMessageChatWindow?: boolean }} ChatWindowData */
|
||||
|
||||
export class ChatWindow extends Record {
|
||||
static id = "threadLocalId";
|
||||
static id = "thread";
|
||||
/** @type {ChatWindow[]} */
|
||||
static records = [];
|
||||
/** @returns {ChatWindow} */
|
||||
@@ -24,7 +24,7 @@ export class ChatWindow extends Record {
|
||||
* @returns {ChatWindow}
|
||||
*/
|
||||
static insert(data = {}) {
|
||||
const chatWindow = this.records.find((c) => c.threadLocalId === data.thread?.localId);
|
||||
const chatWindow = this.records.find((c) => c.thread?.eq(data.thread));
|
||||
if (!chatWindow) {
|
||||
const chatWindow = this.new(data);
|
||||
Object.assign(chatWindow, { thread: data.thread });
|
||||
@@ -57,20 +57,11 @@ export class ChatWindow extends Record {
|
||||
return chatWindow;
|
||||
}
|
||||
|
||||
/** @type {import("@mail/core/common/thread_model").Thread.localId} */
|
||||
threadLocalId;
|
||||
thread = Record.one("Thread");
|
||||
autofocus = 0;
|
||||
folded = false;
|
||||
hidden = false;
|
||||
|
||||
get thread() {
|
||||
return this._store.Thread.records[this.threadLocalId];
|
||||
}
|
||||
|
||||
set thread(thread) {
|
||||
this.threadLocalId = thread?.localId;
|
||||
}
|
||||
|
||||
get displayName() {
|
||||
return this.thread?.displayName ?? _t("New message");
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { OR, Record } from "@mail/core/common/record";
|
||||
*/
|
||||
|
||||
export class Composer extends Record {
|
||||
static id = OR("threadLocalId", "messageLocalId");
|
||||
static id = OR("thread", "message");
|
||||
/** @returns {Composer} */
|
||||
static new(data) {
|
||||
return super.new(data);
|
||||
@@ -57,8 +57,7 @@ export class Composer extends Record {
|
||||
|
||||
/** @type {import("@mail/core/common/attachment_model").Attachment[]} */
|
||||
attachments = [];
|
||||
/** @type {import("@mail/core/common/message_model").Message.localId} */
|
||||
messageLocalId;
|
||||
message = Record.one("Message");
|
||||
/** @type {RawMentions} */
|
||||
rawMentions = {
|
||||
partnerIds: new Set(),
|
||||
@@ -68,8 +67,7 @@ export class Composer extends Record {
|
||||
cannedResponseIds = new Set();
|
||||
/** @type {string} */
|
||||
textInputContent;
|
||||
/** @type {import("@mail/core/common/thread_model").Thread.localId} */
|
||||
threadLocalId;
|
||||
thread = Record.one("Thread");
|
||||
/** @type {{ start: number, end: number, direction: "forward" | "backward" | "none"}}*/
|
||||
selection = {
|
||||
start: 0,
|
||||
@@ -79,26 +77,6 @@ export class Composer extends Record {
|
||||
/** @type {boolean} */
|
||||
forceCursorMove;
|
||||
isFocused = false;
|
||||
|
||||
/** @type {import("@mail/core/common/message_model").Message} */
|
||||
get message() {
|
||||
return this._store.Message.records[this.messageLocalId];
|
||||
}
|
||||
|
||||
/** @param {import("@mail/core/common/message_model").Message} */
|
||||
set message(newMessage) {
|
||||
this.messageLocalId = newMessage?.localId;
|
||||
}
|
||||
|
||||
/** @type {import("@mail/core/common/thread_model").Thread} */
|
||||
get thread() {
|
||||
return this._store.Thread.records[this.threadLocalId];
|
||||
}
|
||||
|
||||
/** @param {import("@mail/core/common/thread_model").Thread} */
|
||||
set thread(newThread) {
|
||||
this.threadLocalId = newThread?.localId;
|
||||
}
|
||||
}
|
||||
|
||||
Composer.register();
|
||||
|
||||
@@ -37,14 +37,12 @@ export class Follower extends Record {
|
||||
return follower;
|
||||
}
|
||||
|
||||
/** @type {import("@mail/core/common/thread_model").Thread} */
|
||||
followedThread;
|
||||
followedThread = Record.one("Thread");
|
||||
/** @type {number} */
|
||||
id;
|
||||
/** @type {boolean} */
|
||||
isActive;
|
||||
/** @type {import("@mail/core/common/persona_model").Persona} */
|
||||
partner;
|
||||
partner = Record.one("Persona");
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
|
||||
@@ -23,7 +23,7 @@ export class LinkPreviewConfirmDelete extends Component {
|
||||
}
|
||||
|
||||
get message() {
|
||||
return this.store.Message.get(this.props.linkPreview.message_id);
|
||||
return this.props.linkPreview.message;
|
||||
}
|
||||
|
||||
onClickOk() {
|
||||
|
||||
@@ -18,6 +18,8 @@ export class LinkPreview extends Record {
|
||||
*/
|
||||
static insert(data) {
|
||||
const message = this.store.Message.get(data.message_id);
|
||||
data.message = message;
|
||||
delete data.message_id;
|
||||
let linkPreview = message?.linkPreviews.find((lp) => lp.id === data.id);
|
||||
if (linkPreview) {
|
||||
return Object.assign(linkPreview, data);
|
||||
@@ -30,8 +32,7 @@ export class LinkPreview extends Record {
|
||||
|
||||
/** @type {number} */
|
||||
id;
|
||||
/** @type {number} */
|
||||
message_id;
|
||||
message = Record.one("Message");
|
||||
/** @type {string} */
|
||||
image_mimetype;
|
||||
/** @type {string} */
|
||||
|
||||
@@ -42,8 +42,7 @@ export class Message extends Record {
|
||||
|
||||
/** @type {Object[]} */
|
||||
attachments = [];
|
||||
/** @type {import("@mail/core/common/persona_model").Persona} */
|
||||
author;
|
||||
author = Record.one("Persona");
|
||||
/** @type {string} */
|
||||
body;
|
||||
/** @type {string} */
|
||||
@@ -64,8 +63,7 @@ export class Message extends Record {
|
||||
needaction_partner_ids = [];
|
||||
/** @type {number[]} */
|
||||
history_partner_ids = [];
|
||||
/** @type {Message|undefined} */
|
||||
parentMessage;
|
||||
parentMessage = Record.one("Message");
|
||||
/** @type {MessageReactions[]} */
|
||||
reactions = [];
|
||||
/** @type {import("@mail/core/common/notification_model").Notification[]} */
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { AND, Record } from "@mail/core/common/record";
|
||||
|
||||
export class MessageReactions extends Record {
|
||||
static id = AND("messageId", "content");
|
||||
static id = AND("message", "content");
|
||||
/** @returns {MessageReactions} */
|
||||
static new(data) {
|
||||
return super.new(data);
|
||||
@@ -48,7 +48,7 @@ export class MessageReactions extends Record {
|
||||
Object.assign(reaction, {
|
||||
count: data.count,
|
||||
content: data.content,
|
||||
messageId: data.message.id,
|
||||
message: data.message,
|
||||
personaLocalIds: reaction.personaLocalIds.filter(
|
||||
(localId) => !personasToUnlink.has(localId)
|
||||
),
|
||||
@@ -62,8 +62,7 @@ export class MessageReactions extends Record {
|
||||
count;
|
||||
/** @type {number[]} */
|
||||
personaLocalIds = [];
|
||||
/** @type {number} */
|
||||
messageId;
|
||||
message = Record.one("Message");
|
||||
|
||||
/** @type {import("@mail/core/common/persona_model").Persona[]} */
|
||||
get personas() {
|
||||
|
||||
@@ -182,7 +182,7 @@ export class MessageService {
|
||||
{
|
||||
action: "remove",
|
||||
content: reaction.content,
|
||||
message_id: reaction.messageId,
|
||||
message_id: reaction.message.id,
|
||||
},
|
||||
{ silent: true }
|
||||
);
|
||||
@@ -284,7 +284,7 @@ export class MessageService {
|
||||
replaceArrayWithCompare(
|
||||
message.notifications,
|
||||
notifications.map((notification) =>
|
||||
this.store.Notification.insert({ ...notification, messageId: message.id })
|
||||
this.store.Notification.insert({ ...notification, message })
|
||||
)
|
||||
);
|
||||
replaceArrayWithCompare(
|
||||
@@ -342,7 +342,7 @@ export class MessageService {
|
||||
|
||||
updateNotification(notification, data) {
|
||||
Object.assign(notification, {
|
||||
messageId: data.messageId,
|
||||
message: data.message,
|
||||
notification_status: data.notification_status,
|
||||
notification_type: data.notification_type,
|
||||
failure_type: data.failure_type,
|
||||
@@ -390,10 +390,10 @@ export class MessageService {
|
||||
group.notifications = group.notifications.filter(
|
||||
({ id }) => !notificationIdsToRemove.has(id)
|
||||
);
|
||||
group.lastMessageId = group.notifications[0]?.message.id;
|
||||
group.lastMessage = group.notifications[0]?.message;
|
||||
for (const notification of group.notifications) {
|
||||
if (group.lastMessageId < notification.message.id) {
|
||||
group.lastMessageId = notification.message.id;
|
||||
if (group.lastMessage?.id < notification.message?.id) {
|
||||
group.lastMessage = notification.message;
|
||||
}
|
||||
}
|
||||
group.resIds.add(data.resId);
|
||||
|
||||
@@ -51,8 +51,7 @@ export class NotificationGroup extends Record {
|
||||
modelName;
|
||||
/** @type {string} */
|
||||
resModel;
|
||||
/** @type {number} */
|
||||
lastMessageId;
|
||||
lastMessage = Record.one("Message");
|
||||
/** @type {Set<number>} */
|
||||
resIds = new Set();
|
||||
/** @type {'sms' | 'email'} */
|
||||
@@ -66,10 +65,6 @@ export class NotificationGroup extends Record {
|
||||
return _t("An error occurred when sending an email");
|
||||
}
|
||||
|
||||
get lastMessage() {
|
||||
return this._store.Message.get(this.lastMessageId);
|
||||
}
|
||||
|
||||
get datetime() {
|
||||
return this.lastMessage?.datetime;
|
||||
}
|
||||
|
||||
@@ -29,20 +29,14 @@ export class Notification extends Record {
|
||||
|
||||
/** @type {number} */
|
||||
id;
|
||||
/** @type {number} */
|
||||
messageId;
|
||||
message = Record.one("Message");
|
||||
/** @type {string} */
|
||||
notification_status;
|
||||
/** @type {string} */
|
||||
notification_type;
|
||||
/** @type {string} */
|
||||
failure_type;
|
||||
/** @type {import("@mail/core/common/persona_model").Persona} */
|
||||
persona;
|
||||
|
||||
get message() {
|
||||
return this._store.Message.get(this.messageId);
|
||||
}
|
||||
persona = Record.one("Persona");
|
||||
|
||||
get isFailure() {
|
||||
return ["exception", "bounce"].includes(this.notification_status);
|
||||
|
||||
@@ -35,8 +35,6 @@ export class Persona extends Record {
|
||||
return persona;
|
||||
}
|
||||
|
||||
/** @type {string} */
|
||||
localId;
|
||||
/** @type {number} */
|
||||
id;
|
||||
/** @type {boolean | undefined} */
|
||||
|
||||
@@ -110,7 +110,7 @@ export class PersonaService {
|
||||
}
|
||||
return b.lastInterestDateTime.ts - a.lastInterestDateTime.ts;
|
||||
})
|
||||
.map((thread) => thread.chatPartnerId);
|
||||
.map((thread) => thread.chatPartner?.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { registry } from "@web/core/registry";
|
||||
|
||||
export const modelRegistry = registry.category("discuss.model");
|
||||
|
||||
const ONE_SYM = Symbol("one");
|
||||
const OR_SYM = Symbol("or");
|
||||
const AND_SYM = Symbol("and");
|
||||
|
||||
@@ -31,7 +32,7 @@ export class Record {
|
||||
}
|
||||
static localId(data) {
|
||||
let idStr;
|
||||
if (typeof data === "object") {
|
||||
if (typeof data === "object" && data !== null) {
|
||||
idStr = this._localId(this.id, data);
|
||||
} else {
|
||||
idStr = data; // non-object data => single id
|
||||
@@ -40,6 +41,10 @@ export class Record {
|
||||
}
|
||||
static _localId(expr, data, { brackets = false } = {}) {
|
||||
if (!Array.isArray(expr)) {
|
||||
if (this.Class.__rels__.has(expr)) {
|
||||
// relational field (note: optional when OR)
|
||||
return `(${data[expr]?.localId})`;
|
||||
}
|
||||
return data[expr];
|
||||
}
|
||||
const vals = [];
|
||||
@@ -79,13 +84,28 @@ export class Record {
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {keyof import("model ").Models} M
|
||||
* @param {M} modelName
|
||||
* @returns {import("models").Models[M]}
|
||||
*/
|
||||
static one(modelName) {
|
||||
return ONE_SYM;
|
||||
}
|
||||
/**
|
||||
* @param {Object} data
|
||||
* @returns {Record}
|
||||
*/
|
||||
static insert(data) {}
|
||||
|
||||
/**
|
||||
* Raw relational values of the record, each of which contains object id(s)
|
||||
* rather than the record(s). This allows data in store and models being normalized,
|
||||
* which eases handling relations notably in when a record gets deleted.
|
||||
*
|
||||
* @type {Map<string, any>}
|
||||
*/
|
||||
__rels__ = new Map();
|
||||
/** @type {import("@mail/core/common/store_service").Store} */
|
||||
_store;
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@ import { reactive } from "@odoo/owl";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { debounce } from "@web/core/utils/timing";
|
||||
import { modelRegistry } from "./record";
|
||||
import { modelRegistry, Record } from "./record";
|
||||
|
||||
export class Store {
|
||||
/** @type {typeof import("@mail/core/web/activity_model").Activity} */
|
||||
@@ -187,9 +187,43 @@ export const storeService = {
|
||||
// work-around: make an object whose prototype is the class, so that static props become
|
||||
// instance props.
|
||||
const entry = Object.assign(Object.create(Model), { env, store: res });
|
||||
entry.Class = Model;
|
||||
// Produce another class with changed prototype, so that there are automatic get/set on relational fields
|
||||
const Class = {
|
||||
[Model.name]: class extends Model {
|
||||
static __rels__ = new Set();
|
||||
constructor() {
|
||||
super();
|
||||
for (const name of this.constructor.__rels__) {
|
||||
// Relational fields contain symbols for detection in original class.
|
||||
// This constructor is called on genuine records:
|
||||
// - 'one' fields => undefined
|
||||
const newVal = undefined;
|
||||
this[name] = newVal;
|
||||
this.__rels__.set(name, newVal);
|
||||
}
|
||||
}
|
||||
},
|
||||
}[Model.name];
|
||||
entry.Class = Class;
|
||||
entry.records = JSON.parse(JSON.stringify(Model.records));
|
||||
res[name] = entry;
|
||||
// Detect relational fields with a dummy record and setup getter/setters on them
|
||||
const obj = new Model();
|
||||
for (const [name, val] of Object.entries(obj)) {
|
||||
if (val !== Record.one()) {
|
||||
continue;
|
||||
}
|
||||
Class.__rels__.add(name);
|
||||
Object.defineProperty(Class.prototype, name, {
|
||||
get() {
|
||||
const localId = this.__rels__.get(name);
|
||||
return res.get(localId);
|
||||
},
|
||||
set(record) {
|
||||
this.__rels__.set(name, record?.localId);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
onChange(res.Thread, "records", () => res.updateBusSubscription());
|
||||
services.ui.bus.addEventListener("resize", () => {
|
||||
|
||||
@@ -24,6 +24,6 @@ export class ThreadIcon extends Component {
|
||||
}
|
||||
|
||||
get chatPartner() {
|
||||
return this.store.Persona.get({ type: "partner", id: this.props.thread.chatPartnerId });
|
||||
return this.props.thread.chatPartner;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,21 +101,18 @@ export class Thread extends Record {
|
||||
areAttachmentsLoaded = false;
|
||||
/** @type {import("@mail/core/common/attachment_model").Attachment[]} */
|
||||
attachments = [];
|
||||
/** @type {integer} */
|
||||
activeRtcSessionId;
|
||||
activeRtcSession = Record.one("RtcSession");
|
||||
/** @type {object|undefined} */
|
||||
channel;
|
||||
/** @type {import("@mail/core/common/channel_member_model").ChannelMember[]} */
|
||||
channelMembers = [];
|
||||
/** @type {Object<number, import("@mail/discuss/call/common/rtc_session_model").RtcSession>} */
|
||||
rtcSessions = {};
|
||||
invitingRtcSessionId;
|
||||
rtcInvitingSession = Record.one("RtcSession");
|
||||
/** @type {Set<number>} */
|
||||
invitedMemberIds = new Set();
|
||||
/** @type {integer} */
|
||||
chatPartnerId;
|
||||
/** @type {import("@mail/core/common/composer_model").Composer} */
|
||||
composer;
|
||||
chatPartner = Record.one("Persona");
|
||||
composer = Record.one("Composer");
|
||||
counter = 0;
|
||||
/** @type {string} */
|
||||
customName;
|
||||
@@ -123,8 +120,7 @@ export class Thread extends Record {
|
||||
description;
|
||||
/** @type {Set<import("@mail/core/common/follower_model").Follower>} */
|
||||
followers = new Set();
|
||||
/** @type {import("@mail/core/common/follower_model").Follower} */
|
||||
selfFollower;
|
||||
selfFollower = Record.one("Follower");
|
||||
/** @type {integer|undefined} */
|
||||
followersCount;
|
||||
isAdmin = false;
|
||||
@@ -133,8 +129,7 @@ export class Thread extends Record {
|
||||
isLoadingAttachments = false;
|
||||
isLoadedDeferred = new Deferred();
|
||||
isLoaded = false;
|
||||
/** @type {import("@mail/core/common/attachment_model").Attachment} */
|
||||
mainAttachment;
|
||||
mainAttachment = Record.one("Attachment");
|
||||
memberCount = 0;
|
||||
message_needaction_counter = 0;
|
||||
message_unread_counter = 0;
|
||||
@@ -197,8 +192,7 @@ export class Thread extends Record {
|
||||
canPostOnReadonly;
|
||||
/** @type {String} */
|
||||
last_interest_dt;
|
||||
/** @type {number} */
|
||||
lastServerMessageId;
|
||||
lastServerMessage = Record.one("Message");
|
||||
/** @type {Boolean} */
|
||||
is_editable;
|
||||
|
||||
@@ -211,14 +205,6 @@ export class Thread extends Record {
|
||||
});
|
||||
}
|
||||
|
||||
get activeRtcSession() {
|
||||
return this._store.RtcSession.get(this.activeRtcSessionId);
|
||||
}
|
||||
|
||||
set activeRtcSession(session) {
|
||||
this.activeRtcSessionId = session?.id;
|
||||
}
|
||||
|
||||
get areAllMembersLoaded() {
|
||||
return this.memberCount === this.channelMembers.length;
|
||||
}
|
||||
@@ -276,12 +262,8 @@ export class Thread extends Record {
|
||||
}
|
||||
|
||||
get displayName() {
|
||||
if (this.type === "chat" && this.chatPartnerId) {
|
||||
return (
|
||||
this.customName ||
|
||||
this._store.Persona.get({ type: "partner", id: this.chatPartnerId })
|
||||
.nameOrDisplayName
|
||||
);
|
||||
if (this.type === "chat" && this.chatPartner) {
|
||||
return this.customName || this.chatPartner.nameOrDisplayName;
|
||||
}
|
||||
if (this.type === "group" && !this.name) {
|
||||
const listFormatter = new Intl.ListFormat(
|
||||
@@ -471,10 +453,6 @@ export class Thread extends Record {
|
||||
return this.memberCount - this.channelMembers.length;
|
||||
}
|
||||
|
||||
get rtcInvitingSession() {
|
||||
return this._store.RtcSession.get(this.invitingRtcSessionId);
|
||||
}
|
||||
|
||||
get hasNeedactionMessages() {
|
||||
return this.needactionMessages.length > 0;
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export class ThreadService {
|
||||
thread.memberCount = results["memberCount"];
|
||||
for (const channelMember of channelMembers) {
|
||||
if (channelMember.persona || channelMember.partner) {
|
||||
this.store.ChannelMember.insert({ ...channelMember, threadId: thread.id });
|
||||
this.store.ChannelMember.insert({ ...channelMember, thread });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -533,7 +533,7 @@ export class ThreadService {
|
||||
return;
|
||||
}
|
||||
return Object.values(this.store.Thread.records).find(
|
||||
(thread) => thread.type === "chat" && thread.chatPartnerId === partner.id
|
||||
(thread) => thread.type === "chat" && thread.chatPartner?.eq(partner)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -707,7 +707,12 @@ export class ThreadService {
|
||||
if (serverData.channel && "message_unread_counter" in serverData.channel) {
|
||||
thread.message_unread_counter = serverData.channel.message_unread_counter;
|
||||
}
|
||||
thread.lastServerMessageId = serverData.last_message_id ?? thread.lastServerMessageId;
|
||||
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);
|
||||
@@ -730,7 +735,7 @@ export class ThreadService {
|
||||
(serverData.channel.channelMembers[0][1].length === 1 &&
|
||||
member.persona?.eq(thread._store.user))
|
||||
) {
|
||||
thread.chatPartnerId = member.persona.id;
|
||||
thread.chatPartner = member.persona;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,9 +166,7 @@ patch(ThreadService.prototype, {
|
||||
thread.suggestedRecipients = recipients;
|
||||
},
|
||||
async leaveChannel(channel) {
|
||||
const chatWindow = this.store.ChatWindow.records.find(
|
||||
(c) => c.threadLocalId === channel.localId
|
||||
);
|
||||
const chatWindow = this.store.ChatWindow.records.find((c) => c.thread?.eq(channel));
|
||||
if (chatWindow) {
|
||||
this.chatWindowService.close(chatWindow);
|
||||
}
|
||||
@@ -249,9 +247,7 @@ patch(ThreadService.prototype, {
|
||||
follower.delete();
|
||||
},
|
||||
unpin(thread) {
|
||||
const chatWindow = this.store.ChatWindow.records.find(
|
||||
(c) => c.threadLocalId === thread.localId
|
||||
);
|
||||
const chatWindow = this.store.ChatWindow.records.find((c) => c.thread?.eq(thread));
|
||||
if (chatWindow) {
|
||||
this.chatWindowService.close(chatWindow);
|
||||
}
|
||||
|
||||
@@ -290,7 +290,9 @@ export class Rtc {
|
||||
thread,
|
||||
record: data.rtc_inviting_session,
|
||||
});
|
||||
thread.invitingRtcSessionId = data.rtc_inviting_session.id;
|
||||
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);
|
||||
}
|
||||
@@ -298,7 +300,7 @@ export class Rtc {
|
||||
if ("rtcInvitingSession" in data) {
|
||||
if (Array.isArray(data.rtcInvitingSession)) {
|
||||
if (data.rtcInvitingSession[0][0] === "unlink") {
|
||||
thread.invitingRtcSessionId = undefined;
|
||||
thread.rtcInvitingSession = undefined;
|
||||
removeFromArray(this.store.ringingThreads, thread.localId);
|
||||
}
|
||||
return;
|
||||
@@ -307,7 +309,9 @@ export class Rtc {
|
||||
thread,
|
||||
record: data.rtcInvitingSession,
|
||||
});
|
||||
thread.invitingRtcSessionId = data.rtcInvitingSession.id;
|
||||
thread.rtcInvitingSession = this.store.RtcSession.insert({
|
||||
id: data.rtcInvitingSession.id,
|
||||
});
|
||||
this.store.ringingThreads.push(thread.localId);
|
||||
}
|
||||
if ("rtcSessions" in data) {
|
||||
|
||||
@@ -29,7 +29,7 @@ export class RtcSession extends Record {
|
||||
}
|
||||
if (channelMember) {
|
||||
const channelMemberRecord = this.store.ChannelMember.insert(channelMember);
|
||||
channelMemberRecord.rtcSessionId = session.id;
|
||||
channelMemberRecord.rtcSession = session;
|
||||
session.channelMemberId = channelMemberRecord.id;
|
||||
if (channelMemberRecord.thread) {
|
||||
channelMemberRecord.thread.rtcSessions[session.id] = session;
|
||||
|
||||
@@ -103,7 +103,7 @@ export class ChannelInvitation extends Component {
|
||||
async onClickInvite() {
|
||||
if (this.props.thread.type === "chat") {
|
||||
await this.discussCoreCommonService.startChat([
|
||||
this.props.thread.chatPartnerId,
|
||||
this.props.thread.chatPartner?.id,
|
||||
...this.state.selectedPartners.map((partner) => partner.id),
|
||||
]);
|
||||
} else {
|
||||
@@ -120,13 +120,13 @@ export class ChannelInvitation extends Component {
|
||||
} else if (this.props.thread.type === "group") {
|
||||
return _t("Invite to Group Chat");
|
||||
} else if (this.props.thread.type === "chat") {
|
||||
if (this.props.thread.chatPartnerId === this.store.self.id) {
|
||||
if (this.props.thread.chatPartner?.eq(this.store.self)) {
|
||||
if (this.state.selectedPartners.length === 0) {
|
||||
return _t("Invite");
|
||||
}
|
||||
if (this.state.selectedPartners.length === 1) {
|
||||
const alreadyChat = Object.values(this.store.Thread.records).some(
|
||||
(thread) => thread.chatPartnerId === this.state.selectedPartners[0].id
|
||||
const alreadyChat = Object.values(this.store.Thread.records).some((thread) =>
|
||||
thread.chatPartner?.eq(this.state.selectedPartners[0])
|
||||
);
|
||||
if (alreadyChat) {
|
||||
return _t("Go to conversation");
|
||||
|
||||
@@ -269,7 +269,7 @@ export class DiscussCoreCommon {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (channel.chatPartnerId !== this.store.odoobot?.id) {
|
||||
if (!channel.chatPartner?.eq(this.store.odoobot)) {
|
||||
if (
|
||||
!this.presence.isOdooFocused() &&
|
||||
channel.isChatChannel &&
|
||||
|
||||
@@ -15,7 +15,7 @@ patch(Thread.prototype, {
|
||||
);
|
||||
}
|
||||
if (this.type === "chat") {
|
||||
return `/web/image/res.partner/${this.chatPartnerId}/avatar_128`;
|
||||
return `/web/image/res.partner/${this.chatPartner.id}/avatar_128`;
|
||||
}
|
||||
return super.imgUrl;
|
||||
},
|
||||
|
||||
@@ -44,14 +44,14 @@ commandProviderRegistry.add("mail.partner", {
|
||||
if (!options.searchValue) {
|
||||
mentionedChannels.slice(0, 3).map((channel) => {
|
||||
if (channel.type === "chat") {
|
||||
displayedPartnerIds.add(channel.chatPartnerId);
|
||||
displayedPartnerIds.add(channel.chatPartner.id);
|
||||
}
|
||||
commands.push({
|
||||
Component: DiscussCommand,
|
||||
async action() {
|
||||
switch (channel.type) {
|
||||
case "chat":
|
||||
threadService.openChat({ partnerId: channel.chatPartnerId });
|
||||
threadService.openChat({ partnerId: channel.chatPartner.id });
|
||||
break;
|
||||
case "group":
|
||||
threadService.open(channel);
|
||||
|
||||
@@ -90,8 +90,8 @@ export class DiscussCoreWeb {
|
||||
if (data.serverFoldState && thread && data.serverFoldState !== thread.state) {
|
||||
thread.state = data.serverFoldState;
|
||||
if (thread.state === "closed") {
|
||||
const chatWindow = this.store.ChatWindow.records.find(
|
||||
(chatWindow) => chatWindow.threadLocalId === thread.localId
|
||||
const chatWindow = this.store.ChatWindow.records.find((chatWindow) =>
|
||||
chatWindow.thread?.eq(thread)
|
||||
);
|
||||
if (chatWindow) {
|
||||
this.chatWindowService.close(chatWindow);
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/* add this file in jsconfig.json, in typeRoots array */
|
||||
declare module "models" {
|
||||
import { Attachment } from "@mail/core/common/attachment_model";
|
||||
import { Composer } from "@mail/core/common/composer_model";
|
||||
import { Follower } from "@mail/core/common/follower_model";
|
||||
import { Message } from "@mail/core/common/message_model";
|
||||
import { Persona } from "@mail/core/common/persona_model";
|
||||
import { RtcSession } from "@mail/discuss/call/common/rtc_session_model";
|
||||
import { Thread } from "@mail/core/common/thread_model";
|
||||
|
||||
export interface Models {
|
||||
"Attachment": Attachment,
|
||||
"Composer": Composer,
|
||||
"Follower": Follower,
|
||||
"Message": Message,
|
||||
"Persona": Persona,
|
||||
"RtcSession": RtcSession,
|
||||
"Thread": Thread,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user