[REF] mail: set data immediately on discuss model relational field
With this commit, instead of `Record.insert()` before setting
relation with records, we can immediately pass record data.
For example:
```js
message.author = { id: 3, type: "partner", name: "Admin" };
thread.messages.add({ id: 10, body: "some-text-content" })
```
This is supported on all relational fields that define a target
model.
To make this work while drastically avoiding cyclic dependencies
in code, whenever data have to be inserted in relation, they are
pre-inserted with essential data, and then they are fully inserted
after being registered in the relation.
closes odoo/odoo#136539
Related: odoo/enterprise#47854
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
@@ -8,8 +8,11 @@ patch(ChatWindow.prototype, {
|
||||
async close(options) {
|
||||
const thread = this.thread;
|
||||
await super.close(options);
|
||||
if (thread?.type === "livechat" && thread.isLoaded && thread.messages.length === 0) {
|
||||
this.threadService.unpin(thread);
|
||||
if (thread?.type === "livechat") {
|
||||
await thread?.isLoadedDeferred;
|
||||
if (thread.messages.length === 0) {
|
||||
this.threadService.unpin(thread);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,4 +2,7 @@ declare module "models" {
|
||||
export interface DiscussApp {
|
||||
livechat: DiscussAppCategory,
|
||||
}
|
||||
export interface Thread {
|
||||
operator: Persona,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { patch } from "@web/core/utils/patch";
|
||||
patch(DiscussApp, {
|
||||
new(data) {
|
||||
const res = super.new(data);
|
||||
res.livechat = this.store.DiscussAppCategory.insert({
|
||||
res.livechat = {
|
||||
extraClass: "o-mail-DiscussSidebarCategory-livechat",
|
||||
id: "livechat",
|
||||
name: _t("Livechat"),
|
||||
@@ -17,7 +17,7 @@ patch(DiscussApp, {
|
||||
canView: false,
|
||||
canAdd: false,
|
||||
serverStateKey: "is_discuss_sidebar_category_livechat_open",
|
||||
});
|
||||
};
|
||||
return res;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { DEFAULT_AVATAR } from "@mail/core/common/persona_service";
|
||||
import { Record } from "@mail/core/common/record";
|
||||
import { Thread } from "@mail/core/common/thread_model";
|
||||
import { assignDefined } from "@mail/utils/common/misc";
|
||||
|
||||
@@ -8,29 +9,30 @@ import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(Thread, {
|
||||
insert(data) {
|
||||
const isUnknown = !this.get(data);
|
||||
const thread = super.insert(data);
|
||||
if (thread.type === "livechat") {
|
||||
if (data?.channel) {
|
||||
assignDefined(thread, data.channel, ["anonymous_name"]);
|
||||
}
|
||||
if (data?.operator_pid) {
|
||||
thread.operator = this.store.Persona.insert({
|
||||
thread.operator = {
|
||||
type: "partner",
|
||||
id: data.operator_pid[0],
|
||||
displayName: data.operator_pid[1],
|
||||
});
|
||||
}
|
||||
if (isUnknown) {
|
||||
this.store.discuss.livechat.threads.push(thread);
|
||||
this.env.services["mail.thread"].sortChannels();
|
||||
};
|
||||
}
|
||||
this.store.discuss.livechat.threads.add(thread);
|
||||
this.env.services["mail.thread"].sortChannels();
|
||||
}
|
||||
return thread;
|
||||
},
|
||||
});
|
||||
|
||||
patch(Thread.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.operator = Record.one("Persona");
|
||||
},
|
||||
get typesAllowingCalls() {
|
||||
return super.typesAllowingCalls.concat(["livechat"]);
|
||||
},
|
||||
|
||||
@@ -131,9 +131,7 @@ export class ChatBotService {
|
||||
channel_uuid: this.livechatService.thread.uuid,
|
||||
chatbot_script_id: this.chatbot.scriptId,
|
||||
});
|
||||
this.livechatService.thread?.messages.push(
|
||||
this.store.Message.insert({ ...message, body: markup(message.body) })
|
||||
);
|
||||
this.livechatService.thread?.messages.push({ ...message, body: markup(message.body) });
|
||||
this.currentStep = null;
|
||||
this.start();
|
||||
}
|
||||
@@ -147,11 +145,10 @@ export class ChatBotService {
|
||||
chatbot_script_id: this.chatbot.scriptId,
|
||||
});
|
||||
for (const rawMessage of rawMessages) {
|
||||
const message = this.store.Message.insert({
|
||||
this.livechatService.thread?.messages.add({
|
||||
...rawMessage,
|
||||
body: markup(rawMessage.body),
|
||||
});
|
||||
this.livechatService.thread?.messages.add(message);
|
||||
}
|
||||
this.hasPostedWelcomeSteps = true;
|
||||
}
|
||||
@@ -180,11 +177,10 @@ export class ChatBotService {
|
||||
return;
|
||||
}
|
||||
if (stepMessage) {
|
||||
const message = this.store.Message.insert({
|
||||
this.livechatService.thread?.messages.add({
|
||||
...stepMessage,
|
||||
body: markup(stepMessage.body),
|
||||
});
|
||||
this.livechatService.thread?.messages.add(message);
|
||||
}
|
||||
this.currentStep = step;
|
||||
if (
|
||||
@@ -281,10 +277,8 @@ export class ChatBotService {
|
||||
channel_uuid: this.livechatService.thread.uuid,
|
||||
});
|
||||
this.currentStep.isEmailValid = success;
|
||||
if (msg && !this.livechatService.thread.messages.some((m) => m.id === msg.id)) {
|
||||
this.livechatService.thread.messages.push(
|
||||
this.store.Message.insert({ ...msg, body: markup(msg.body) })
|
||||
);
|
||||
if (msg) {
|
||||
this.livechatService.thread.messages.add({ ...msg, body: markup(msg.body) });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
declare module "models" {
|
||||
export interface Thread {
|
||||
chatbotTypingMessage: Message,
|
||||
livechatWelcomeMessage: Message,
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,10 @@ patch(Messaging.prototype, {
|
||||
return super.initialize();
|
||||
}
|
||||
if (session.livechatData?.options.current_partner_id) {
|
||||
this.store.user = this.store.Persona.insert({
|
||||
this.store.user = {
|
||||
type: "partner",
|
||||
id: session.livechatData.options.current_partner_id,
|
||||
});
|
||||
};
|
||||
}
|
||||
this.store.isMessagingReady = true;
|
||||
this.isReady.resolve({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { Record } from "@mail/core/common/record";
|
||||
import { Thread } from "@mail/core/common/thread_model";
|
||||
import { onChange } from "@mail/utils/common/misc";
|
||||
|
||||
@@ -25,20 +26,20 @@ patch(Thread, {
|
||||
}
|
||||
});
|
||||
if (chatbotService.isChatbotThread(thread)) {
|
||||
thread.chatbotTypingMessage = this.store.Message.insert({
|
||||
thread.chatbotTypingMessage = {
|
||||
id: messageService.getNextTemporaryId(),
|
||||
res_id: thread.id,
|
||||
model: thread.model,
|
||||
author: thread.operator,
|
||||
});
|
||||
};
|
||||
} else {
|
||||
thread.livechatWelcomeMessage = this.store.Message.insert({
|
||||
thread.livechatWelcomeMessage = {
|
||||
id: messageService.getNextTemporaryId(),
|
||||
body: livechatService.options.default_message,
|
||||
res_id: thread.id,
|
||||
model: thread.model,
|
||||
author: thread.operator,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
return thread;
|
||||
@@ -48,14 +49,19 @@ patch(Thread, {
|
||||
patch(Thread.prototype, {
|
||||
chatbotScriptId: null,
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.chatbotTypingMessage = Record.one("Message");
|
||||
this.livechatWelcomeMessage = Record.one("Message");
|
||||
},
|
||||
update(data) {
|
||||
super.update(...arguments);
|
||||
if (data.operator_pid) {
|
||||
this.operator = this._store.Persona.insert({
|
||||
this.operator = {
|
||||
type: "partner",
|
||||
id: data.operator_pid[0],
|
||||
name: data.operator_pid[1],
|
||||
});
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ patch(ThreadService.prototype, {
|
||||
},
|
||||
|
||||
avatarUrl(author, thread) {
|
||||
if (thread.type !== "livechat") {
|
||||
if (thread?.type !== "livechat") {
|
||||
return super.avatarUrl(...arguments);
|
||||
}
|
||||
const isFromOperator =
|
||||
|
||||
@@ -5,7 +5,7 @@ import { startServer } from "@bus/../tests/helpers/mock_python_environment";
|
||||
import { loadDefaultConfig, start } from "@im_livechat/../tests/embed/helper/test_utils";
|
||||
import { LivechatButton } from "@im_livechat/embed/core_ui/livechat_button";
|
||||
|
||||
import { mockTimeout, triggerHotkey } from "@web/../tests/helpers/utils";
|
||||
import { mockTimeout, nextTick, triggerHotkey } from "@web/../tests/helpers/utils";
|
||||
import { click, contains, insertText } from "@web/../tests/utils";
|
||||
|
||||
QUnit.module("livechat session");
|
||||
@@ -57,6 +57,7 @@ QUnit.test("Seen message is saved on the session", async (assert) => {
|
||||
await insertText(".o-mail-Composer-input", "Hello World!");
|
||||
triggerHotkey("Enter");
|
||||
await contains(".o-mail-Message", { count: 2 });
|
||||
await nextTick(); // wait for message seen
|
||||
assert.strictEqual(
|
||||
env.services["im_livechat.livechat"].sessionCookie.seen_message_id,
|
||||
env.services["im_livechat.livechat"].thread.newestMessage.id
|
||||
|
||||
@@ -11,10 +11,6 @@ export class Attachment extends Record {
|
||||
/** @type {Object.<number, import("models").Attachment>} */
|
||||
static records = {};
|
||||
/** @returns {import("models").Attachment} */
|
||||
static new(data) {
|
||||
return super.new(data);
|
||||
}
|
||||
/** @returns {import("models").Attachment} */
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
@@ -26,7 +22,8 @@ export class Attachment extends Record {
|
||||
if (!("id" in data)) {
|
||||
throw new Error("Cannot insert attachment: id is missing in data");
|
||||
}
|
||||
const attachment = this.get(data) ?? this.new(data);
|
||||
/** @type {import("models").Attachment} */
|
||||
const attachment = this.preinsert(data);
|
||||
Object.assign(attachment, { id: data.id });
|
||||
attachment.update(data);
|
||||
return attachment;
|
||||
@@ -55,10 +52,10 @@ export class Attachment extends Record {
|
||||
const threadData = Array.isArray(data.originThread)
|
||||
? data.originThread[0][1]
|
||||
: data.originThread;
|
||||
this.originThread = this._store.Thread.insert({
|
||||
this.originThread = {
|
||||
model: threadData.model,
|
||||
id: threadData.id,
|
||||
});
|
||||
};
|
||||
const thread = this.originThread;
|
||||
thread.attachments.add(this);
|
||||
thread.attachments.sort((a1, a2) => (a1.id < a2.id ? 1 : -1));
|
||||
|
||||
@@ -14,7 +14,7 @@ export class AutoresizeInput extends Component {
|
||||
enabled: { optional: true },
|
||||
onValidate: { type: Function, optional: true },
|
||||
placeholder: { type: String, optional: true },
|
||||
value: { type: String },
|
||||
value: { type: String, optional: true },
|
||||
};
|
||||
static defaultProps = {
|
||||
autofocus: false,
|
||||
|
||||
@@ -7,10 +7,6 @@ export class CannedResponse extends Record {
|
||||
/** @type {Object.<number, import("models").CannedResponse>} */
|
||||
static records = {};
|
||||
/** @returns {import("models").CannedResponse} */
|
||||
static new(data) {
|
||||
return super.new(data);
|
||||
}
|
||||
/** @returns {import("models").CannedResponse} */
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
@@ -19,7 +15,8 @@ export class CannedResponse extends Record {
|
||||
* @returns {import("models").CannedResponse}
|
||||
*/
|
||||
static insert(data) {
|
||||
const cannedResponse = this.get(data) ?? this.new(data);
|
||||
/** @type {import("models").CannedResponse} */
|
||||
const cannedResponse = this.preinsert(data);
|
||||
Object.assign(cannedResponse, {
|
||||
id: data.id,
|
||||
name: data.source,
|
||||
|
||||
@@ -14,10 +14,6 @@ export class ChannelMember extends Record {
|
||||
/** @type {Object.<number, import("models").ChannelMember>} */
|
||||
static records = {};
|
||||
/** @returns {import("models").ChannelMember} */
|
||||
static new(data) {
|
||||
return super.new(data);
|
||||
}
|
||||
/** @returns {import("models").ChannelMember} */
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
@@ -27,7 +23,8 @@ export class ChannelMember extends Record {
|
||||
*/
|
||||
static insert(data) {
|
||||
const memberData = Array.isArray(data) ? data[1] : data;
|
||||
const member = this.get(memberData) ?? this.new(memberData);
|
||||
/** @type {import("models").ChannelMember} */
|
||||
const member = this.preinsert(memberData);
|
||||
member.update(data);
|
||||
return member;
|
||||
}
|
||||
@@ -36,19 +33,19 @@ export class ChannelMember extends Record {
|
||||
const [command, memberData] = Array.isArray(data) ? data : ["ADD", data];
|
||||
this.id = memberData.id;
|
||||
if ("persona" in memberData) {
|
||||
this.persona = this._store.Persona.insert({
|
||||
this.persona = {
|
||||
...(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({
|
||||
thread = {
|
||||
id: memberData.channel.id,
|
||||
model: "discuss.channel",
|
||||
});
|
||||
};
|
||||
}
|
||||
if (thread && !this.thread) {
|
||||
this.thread = thread;
|
||||
|
||||
@@ -12,10 +12,6 @@ export class ChatWindow extends Record {
|
||||
/** @type {Object<number, import("models").ChatWindow} */
|
||||
static records = {};
|
||||
/** @returns {import("models").ChatWindow} */
|
||||
static new(data) {
|
||||
return super.new(data);
|
||||
}
|
||||
/** @returns {import("models").ChatWindow} */
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
@@ -26,7 +22,8 @@ export class ChatWindow extends Record {
|
||||
static insert(data = {}) {
|
||||
const chatWindow = this.store.discuss.chatWindows.find((c) => c.thread?.eq(data.thread));
|
||||
if (!chatWindow) {
|
||||
const chatWindow = this.new(data);
|
||||
/** @type {import("models").ChatWindow} */
|
||||
const chatWindow = this.preinsert(data);
|
||||
Object.assign(chatWindow, { thread: data.thread });
|
||||
assignDefined(chatWindow, data);
|
||||
let index;
|
||||
@@ -54,7 +51,7 @@ export class ChatWindow extends Record {
|
||||
data.replaceNewMessageChatWindow ? 1 : 0,
|
||||
chatWindow
|
||||
);
|
||||
return this.store.discuss.chatWindows[index]; // return reactive version
|
||||
return chatWindow; // return reactive version
|
||||
}
|
||||
if (chatWindow.hidden) {
|
||||
this.env.services["mail.chat_window"].makeVisible(chatWindow);
|
||||
|
||||
@@ -9,10 +9,6 @@ import { OR, Record } from "@mail/core/common/record";
|
||||
export class Composer extends Record {
|
||||
static id = OR("thread", "message");
|
||||
/** @returns {import("models").Composer} */
|
||||
static new(data) {
|
||||
return super.new(data);
|
||||
}
|
||||
/** @returns {import("models").Composer} */
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
@@ -27,7 +23,8 @@ export class Composer extends Record {
|
||||
}
|
||||
let composer = (thread ?? message)?.composer;
|
||||
if (!composer) {
|
||||
composer = this.new(data);
|
||||
/** @type {import("models").Composer} */
|
||||
composer = this.preinsert(data);
|
||||
const { message, thread } = data;
|
||||
if (thread) {
|
||||
composer.thread = thread;
|
||||
|
||||
@@ -6,16 +6,13 @@ import { Record } from "./record";
|
||||
export class DiscussAppCategory extends Record {
|
||||
static id = "id";
|
||||
/** @returns {import("models").DiscussAppCategory} */
|
||||
static new(data) {
|
||||
return super.new(data);
|
||||
}
|
||||
/** @returns {import("models").DiscussAppCategory} */
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
/** @returns {import("models").DiscussAppCategory} */
|
||||
static insert(data) {
|
||||
const category = this.get(data) ?? this.new(data);
|
||||
/** @type {import("models").DiscussAppCategory} */
|
||||
const category = this.preinsert(data);
|
||||
assignDefined(category, data);
|
||||
return category;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import { _t } from "@web/core/l10n/translation";
|
||||
import { Record } from "./record";
|
||||
|
||||
export class DiscussApp extends Record {
|
||||
/** @returns {import("models").DiscussApp} */
|
||||
static new(data) {
|
||||
/** @type {import("models").DiscussApp} */
|
||||
const res = super.new(data);
|
||||
Object.assign(res, {
|
||||
channels: this.store.DiscussAppCategory.insert({
|
||||
channels: {
|
||||
extraClass: "o-mail-DiscussSidebarCategory-channel",
|
||||
id: "channels",
|
||||
name: _t("Channels"),
|
||||
@@ -18,8 +18,8 @@ export class DiscussApp extends Record {
|
||||
serverStateKey: "is_discuss_sidebar_category_channel_open",
|
||||
addTitle: _t("Add or join a channel"),
|
||||
addHotkey: "c",
|
||||
}),
|
||||
chats: this.store.DiscussAppCategory.insert({
|
||||
},
|
||||
chats: {
|
||||
extraClass: "o-mail-DiscussSidebarCategory-chat",
|
||||
id: "chats",
|
||||
name: _t("Direct messages"),
|
||||
@@ -29,7 +29,7 @@ export class DiscussApp extends Record {
|
||||
serverStateKey: "is_discuss_sidebar_category_chat_open",
|
||||
addTitle: _t("Start a conversation"),
|
||||
addHotkey: "d",
|
||||
}),
|
||||
},
|
||||
});
|
||||
return res;
|
||||
}
|
||||
@@ -38,9 +38,8 @@ export class DiscussApp extends Record {
|
||||
return super.get(data);
|
||||
}
|
||||
/** @returns {import("models").DiscussApp} */
|
||||
static insert() {
|
||||
const app = this.get() ?? this.new();
|
||||
return app;
|
||||
static insert(data) {
|
||||
return super.insert(data);
|
||||
}
|
||||
|
||||
/** @type {'mailbox'|'all'|'channel'|'chat'|'livechat'} */
|
||||
|
||||
@@ -15,10 +15,6 @@ export class Follower extends Record {
|
||||
/** @type {Object.<number, import("models").Follower>} */
|
||||
static records = {};
|
||||
/** @returns {import("models").Follower} */
|
||||
static new(data) {
|
||||
return super.new(data);
|
||||
}
|
||||
/** @returns {import("models").Follower} */
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
@@ -27,12 +23,13 @@ export class Follower extends Record {
|
||||
* @returns {import("models").Follower}
|
||||
*/
|
||||
static insert(data) {
|
||||
const follower = this.get(data) ?? this.new(data);
|
||||
/** @type {import("models").Follower} */
|
||||
const follower = this.preinsert(data);
|
||||
Object.assign(follower, {
|
||||
followedThread: data.followedThread,
|
||||
id: data.id,
|
||||
isActive: data.is_active,
|
||||
partner: this.store.Persona.insert({ ...data.partner, type: "partner" }),
|
||||
partner: { ...data.partner, type: "partner" },
|
||||
});
|
||||
return follower;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,6 @@ import { Record } from "@mail/core/common/record";
|
||||
export class LinkPreview extends Record {
|
||||
static id = "id";
|
||||
/** @returns {import("models").LinkPreview} */
|
||||
static new(data) {
|
||||
return super.new(data);
|
||||
}
|
||||
/** @returns {import("models").LinkPreview} */
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
@@ -20,13 +16,10 @@ export class LinkPreview extends Record {
|
||||
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);
|
||||
}
|
||||
linkPreview = this.new(data);
|
||||
/** @type {import("models").LinkPreview} */
|
||||
const linkPreview = this.preinsert(data);
|
||||
Object.assign(linkPreview, data);
|
||||
message?.linkPreviews.push(linkPreview);
|
||||
message?.linkPreviews.add(linkPreview);
|
||||
return linkPreview;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* @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";
|
||||
|
||||
@@ -19,10 +18,6 @@ export class Message extends Record {
|
||||
/** @type {Object.<number, import("models").Message>} */
|
||||
static records = {};
|
||||
/** @returns {import("models").Message} */
|
||||
static new(data) {
|
||||
return super.new(data);
|
||||
}
|
||||
/** @returns {import("models").Message} */
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
@@ -37,7 +32,8 @@ export class Message extends Record {
|
||||
id: data.res_id,
|
||||
});
|
||||
}
|
||||
const message = this.get(data) ?? this.new(data);
|
||||
/** @type {import("models").Message} */
|
||||
const message = this.preinsert(data);
|
||||
message.update(data);
|
||||
return message;
|
||||
}
|
||||
@@ -72,7 +68,7 @@ export class Message extends Record {
|
||||
? this.starred_partner_ids.includes(this._store.user.id)
|
||||
: false,
|
||||
isTransient,
|
||||
parentMessage: parentMessage ? this._store.Message.insert(parentMessage) : undefined,
|
||||
parentMessage: parentMessage || undefined,
|
||||
resId,
|
||||
resModel,
|
||||
subtypeDescription,
|
||||
@@ -89,48 +85,23 @@ export class Message extends Record {
|
||||
: record_name || undefined,
|
||||
});
|
||||
}
|
||||
replaceArrayWithCompare(
|
||||
this.attachments,
|
||||
attachments.map((attachment) =>
|
||||
this._store.Attachment.insert({ message: this, ...attachment })
|
||||
)
|
||||
);
|
||||
this.attachments = attachments.map((attachment) => ({ message: this, ...attachment }));
|
||||
if (data.author?.id) {
|
||||
this.author = this._store.Persona.insert({
|
||||
...data.author,
|
||||
type: "partner",
|
||||
});
|
||||
this.author = { ...data.author, type: "partner" };
|
||||
}
|
||||
if (data.guestAuthor?.id) {
|
||||
this.author = this._store.Persona.insert({
|
||||
...data.guestAuthor,
|
||||
type: "guest",
|
||||
channelId: this.originThread.id,
|
||||
});
|
||||
this.author = { ...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" })
|
||||
)
|
||||
);
|
||||
this.linkPreviews = linkPreviews.map((data) => ({ ...data, message: this }));
|
||||
this.notifications = notifications.map((notif) => ({ ...notif, message: this }));
|
||||
this.recipients = recipients.map((recipient) => ({ ...recipient, type: "partner" }));
|
||||
if ("user_follower_id" in data && data.user_follower_id && this._store.self) {
|
||||
this.originThread.selfFollower = this._store.Follower.insert({
|
||||
this.originThread.selfFollower = {
|
||||
followedThread: this.originThread,
|
||||
id: data.user_follower_id,
|
||||
isActive: true,
|
||||
partner: this._store.self,
|
||||
});
|
||||
};
|
||||
}
|
||||
if (data.messageReactionGroups) {
|
||||
const reactionContentToUnlink = new Set();
|
||||
|
||||
@@ -5,10 +5,6 @@ import { AND, Record } from "@mail/core/common/record";
|
||||
export class MessageReactions extends Record {
|
||||
static id = AND("message", "content");
|
||||
/** @returns {import("models").MessageReactions} */
|
||||
static new(data) {
|
||||
return super.new(data);
|
||||
}
|
||||
/** @returns {import("models").MessageReactions} */
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
@@ -21,7 +17,8 @@ export class MessageReactions extends Record {
|
||||
({ content }) => content === data.content
|
||||
);
|
||||
if (!reaction) {
|
||||
reaction = this.new(data);
|
||||
/** @type {import("models").MessageReactions} */
|
||||
reaction = this.preinsert(data);
|
||||
}
|
||||
const personasToUnlink = new Set();
|
||||
const alreadyKnownPersonaIds = new Set(reaction.personas.map((p) => p.localId));
|
||||
@@ -48,7 +45,7 @@ export class MessageReactions extends Record {
|
||||
Object.assign(reaction, {
|
||||
count: data.count,
|
||||
content: data.content,
|
||||
message: this.store.Message.insert(data.message),
|
||||
message: data.message,
|
||||
personas: reaction.personas.filter((p) => !personasToUnlink.has(p)),
|
||||
});
|
||||
return reaction;
|
||||
|
||||
@@ -30,26 +30,26 @@ export class Messaging {
|
||||
this.store.Persona.insert({ id: user.partnerId, type: "partner", isAdmin: user.isAdmin });
|
||||
this.registeredImStatusPartners = reactive([], () => this.updateImStatusRegistration());
|
||||
this.store.registeredImStatusPartners = this.registeredImStatusPartners;
|
||||
this.store.discuss.inbox = this.store.Thread.insert({
|
||||
this.store.discuss.inbox = {
|
||||
id: "inbox",
|
||||
model: "mail.box",
|
||||
name: _t("Inbox"),
|
||||
type: "mailbox",
|
||||
});
|
||||
this.store.discuss.starred = this.store.Thread.insert({
|
||||
};
|
||||
this.store.discuss.starred = {
|
||||
id: "starred",
|
||||
model: "mail.box",
|
||||
name: _t("Starred"),
|
||||
type: "mailbox",
|
||||
counter: 0,
|
||||
});
|
||||
this.store.discuss.history = this.store.Thread.insert({
|
||||
};
|
||||
this.store.discuss.history = {
|
||||
id: "history",
|
||||
model: "mail.box",
|
||||
name: _t("History"),
|
||||
type: "mailbox",
|
||||
counter: 0,
|
||||
});
|
||||
};
|
||||
this.updateImStatusRegistration();
|
||||
}
|
||||
|
||||
@@ -64,22 +64,16 @@ export class Messaging {
|
||||
|
||||
initMessagingCallback(data) {
|
||||
if (data.current_partner) {
|
||||
this.store.user = this.store.Persona.insert({
|
||||
...data.current_partner,
|
||||
type: "partner",
|
||||
});
|
||||
this.store.user = { ...data.current_partner, type: "partner" };
|
||||
}
|
||||
if (data.currentGuest) {
|
||||
this.store.guest = this.store.Persona.insert({
|
||||
this.store.guest = {
|
||||
...data.currentGuest,
|
||||
type: "guest",
|
||||
channelId: data.channels[0]?.id,
|
||||
});
|
||||
};
|
||||
}
|
||||
this.store.odoobot = this.store.Persona.insert({
|
||||
...data.odoobot,
|
||||
type: "partner",
|
||||
});
|
||||
this.store.odoobot = { ...data.odoobot, type: "partner" };
|
||||
const settings = data.current_user_settings;
|
||||
this.userSettingsService.updateFromCommands(settings);
|
||||
this.userSettingsService.id = settings.id;
|
||||
|
||||
@@ -10,10 +10,6 @@ export class NotificationGroup extends Record {
|
||||
/** @type {Object.<number, import("models").NotificationGroup>} */
|
||||
static records = {};
|
||||
/** @returns {import("models").NotificationGroup} */
|
||||
static new(data) {
|
||||
return super.new(data);
|
||||
}
|
||||
/** @returns {import("models").NotificationGroup} */
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
@@ -31,7 +27,8 @@ export class NotificationGroup extends Record {
|
||||
});
|
||||
if (!group) {
|
||||
const id = nextId++;
|
||||
group = this.new({ id });
|
||||
/** @type {import("models").NotificationGroup} */
|
||||
group = this.preinsert({ id });
|
||||
Object.assign(group, { id });
|
||||
this.store.discuss.notificationGroups.add(group);
|
||||
}
|
||||
|
||||
@@ -9,10 +9,6 @@ export class Notification extends Record {
|
||||
/** @type {Object.<number, import("models").Notification>} */
|
||||
static records = {};
|
||||
/** @returns {import("models").Notification} */
|
||||
static new(data) {
|
||||
return super.new(data);
|
||||
}
|
||||
/** @returns {import("models").Notification} */
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
@@ -21,7 +17,8 @@ export class Notification extends Record {
|
||||
* @returns {import("models").Notification}
|
||||
*/
|
||||
static insert(data) {
|
||||
const notification = this.get(data) ?? this.new(data);
|
||||
/** @type {import("models").Notification} */
|
||||
const notification = this.preinsert(data);
|
||||
Object.assign(notification, { id: data.id });
|
||||
notification.update(data);
|
||||
return notification;
|
||||
@@ -34,11 +31,11 @@ export class Notification extends Record {
|
||||
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)) {
|
||||
|
||||
@@ -18,10 +18,6 @@ export class Persona extends Record {
|
||||
/** @type {Object.<number, import("models").Persona>} */
|
||||
static records = {};
|
||||
/** @returns {import("models").Persona} */
|
||||
static new(data) {
|
||||
return super.new(data);
|
||||
}
|
||||
/** @returns {import("models").Persona} */
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
@@ -30,10 +26,7 @@ export class Persona extends Record {
|
||||
* @returns {import("models").Persona}
|
||||
*/
|
||||
static insert(data) {
|
||||
const persona = this.get(data) ?? this.new(data);
|
||||
persona.update(data);
|
||||
// return reactive version
|
||||
return persona;
|
||||
return super.insert(data);
|
||||
}
|
||||
|
||||
update(data) {
|
||||
|
||||
@@ -17,6 +17,29 @@ export function OR(...args) {
|
||||
return [OR_SYM, ...args];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {R|any} val
|
||||
* @param {Record} record
|
||||
* @param {string} fname
|
||||
* @param {(R) => void} fn
|
||||
*/
|
||||
export function preinsert(val, record, fname, fn) {
|
||||
/** @type {R} */
|
||||
let r3;
|
||||
if (!(val instanceof Record)) {
|
||||
const { targetModel } = record.Model.__rels__.get(fname);
|
||||
r3 = record.Model.store[targetModel].preinsert(val);
|
||||
} else {
|
||||
r3 = val;
|
||||
}
|
||||
fn(r3);
|
||||
if (!(val instanceof Record)) {
|
||||
// was preinserted, fully insert now
|
||||
const { targetModel } = record.Model.__rels__.get(fname);
|
||||
record.Model.store[targetModel].insert(val);
|
||||
}
|
||||
}
|
||||
|
||||
export class RecordInverses {
|
||||
/**
|
||||
* Track the inverse of a record. Each record contains this map.
|
||||
@@ -102,16 +125,16 @@ export class RecordList extends Array {
|
||||
if (typeof name !== "symbol" && !window.isNaN(parseInt(name))) {
|
||||
// support for "array[index] = r3" syntax
|
||||
const index = parseInt(name);
|
||||
/** @type {R} */
|
||||
const r3 = val;
|
||||
const r2 = receiver[index];
|
||||
if (r2 && r2.notEq(r3)) {
|
||||
receiver.__deleteInverse__(r2);
|
||||
}
|
||||
receiver.__list__[index] = r3?.localId;
|
||||
if (r3) {
|
||||
receiver.__addInverse__(r3);
|
||||
}
|
||||
receiver._preinsert(val, (r3) => {
|
||||
const r2 = receiver[index];
|
||||
if (r2 && r2.notEq(r3)) {
|
||||
receiver.__deleteInverse__(r2);
|
||||
}
|
||||
receiver.__list__[index] = r3?.localId;
|
||||
if (r3) {
|
||||
receiver.__addInverse__(r3);
|
||||
}
|
||||
});
|
||||
} else if (name === "length") {
|
||||
const newLength = parseInt(val);
|
||||
if (newLength < receiver.length) {
|
||||
@@ -125,7 +148,13 @@ export class RecordList extends Array {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {R|any} val
|
||||
* @param {(R) => void} fn
|
||||
*/
|
||||
_preinsert(val, fn) {
|
||||
preinsert(val, this.owner, this.name, fn);
|
||||
}
|
||||
/**
|
||||
* @param {number} index
|
||||
* @returns {R}
|
||||
@@ -135,11 +164,11 @@ export class RecordList extends Array {
|
||||
}
|
||||
/** @param {R[]} records */
|
||||
push(...records) {
|
||||
this.__list__.push(...records.map((r3) => r3.localId));
|
||||
for (const r3 of records) {
|
||||
if (r3) {
|
||||
for (const val of records) {
|
||||
this._preinsert(val, (r3) => {
|
||||
this.__list__.push(r3.localId);
|
||||
this.__addInverse__(r3);
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.__list__.length;
|
||||
}
|
||||
@@ -161,11 +190,11 @@ export class RecordList extends Array {
|
||||
}
|
||||
/** @param {R[]} records */
|
||||
unshift(...records) {
|
||||
this.__list__.unshift(...records.map((r3) => r3.localId));
|
||||
for (const r3 of records) {
|
||||
if (r3) {
|
||||
for (const val of records) {
|
||||
this._preinsert(val, (r3) => {
|
||||
this.__list__.unshift(r3.localId);
|
||||
this.__addInverse__(r3);
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.__list__.length;
|
||||
}
|
||||
@@ -251,20 +280,31 @@ export class RecordList extends Array {
|
||||
.map((localId) => this.__store__.get(localId))
|
||||
.concat(...collections.map((c) => [...c]));
|
||||
}
|
||||
/** @param {R} */
|
||||
add(r) {
|
||||
if (this.indexOf(r) !== -1) {
|
||||
return;
|
||||
/** @param {...R} */
|
||||
add(...records) {
|
||||
for (const val of records) {
|
||||
this._preinsert(val, (r) => {
|
||||
if (this.indexOf(r) === -1) {
|
||||
this.push(r);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.push(r);
|
||||
}
|
||||
/** @param {R} */
|
||||
delete(r) {
|
||||
const index = this.indexOf(r);
|
||||
if (index === -1) {
|
||||
return;
|
||||
/** @param {...R} */
|
||||
delete(...records) {
|
||||
for (const val of records) {
|
||||
this._preinsert(val, (r) => {
|
||||
const index = this.indexOf(r);
|
||||
if (index !== -1) {
|
||||
this.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
clear() {
|
||||
while (this.__list__.length > 0) {
|
||||
this.pop();
|
||||
}
|
||||
this.splice(index, 1);
|
||||
}
|
||||
/** @yields {R} */
|
||||
*[Symbol.iterator]() {
|
||||
@@ -306,7 +346,7 @@ export class Record {
|
||||
}
|
||||
static _localId(expr, data, { brackets = false } = {}) {
|
||||
if (!Array.isArray(expr)) {
|
||||
if (this.Class.__rels__.has(expr)) {
|
||||
if (this.__rels__.has(expr)) {
|
||||
// relational field (note: optional when OR)
|
||||
return `(${data[expr]?.localId})`;
|
||||
}
|
||||
@@ -322,6 +362,35 @@ export class Record {
|
||||
}
|
||||
return res;
|
||||
}
|
||||
static _retrieveIdFromData(data) {
|
||||
const res = {};
|
||||
function _deepRetrieve(expr2) {
|
||||
if (typeof expr2 === "string") {
|
||||
return Object.assign(res, { [expr2]: data[expr2] });
|
||||
}
|
||||
if (expr2 instanceof Array) {
|
||||
for (const expr of this.id) {
|
||||
if (typeof expr === "symbol") {
|
||||
continue;
|
||||
}
|
||||
_deepRetrieve(expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.id === undefined) {
|
||||
return res;
|
||||
}
|
||||
if (typeof this.id === "string") {
|
||||
return { [this.id]: data[this.id] };
|
||||
}
|
||||
for (const expr of this.id) {
|
||||
if (typeof expr === "symbol") {
|
||||
continue;
|
||||
}
|
||||
_deepRetrieve(expr);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* Technical attribute, DO NOT USE in business code.
|
||||
* This class is almost equivalent to current class of model,
|
||||
@@ -340,7 +409,8 @@ export class Record {
|
||||
*/
|
||||
static new(data) {
|
||||
const obj = new this.Class();
|
||||
let record = Object.assign(obj, { localId: this.localId(data), Model: this });
|
||||
const ids = this._retrieveIdFromData(data);
|
||||
let record = Object.assign(obj, { Model: this, localId: this.localId(data), ...ids });
|
||||
Object.assign(record, { _store: this.store });
|
||||
this.records[record.localId] = record;
|
||||
// return reactive version
|
||||
@@ -353,7 +423,7 @@ export class Record {
|
||||
* @returns {import("models").Models[M]}
|
||||
*/
|
||||
static one(modelName) {
|
||||
return ONE_SYM;
|
||||
return [ONE_SYM, modelName];
|
||||
}
|
||||
/**
|
||||
* @template {keyof import("model").Models} M
|
||||
@@ -361,13 +431,24 @@ export class Record {
|
||||
* @returns {import("models").Models[M][]}
|
||||
*/
|
||||
static many(modelName) {
|
||||
return MANY_SYM;
|
||||
return [MANY_SYM, modelName];
|
||||
}
|
||||
/**
|
||||
* @param {Object} data
|
||||
* @returns {Record}
|
||||
*/
|
||||
static insert(data) {}
|
||||
static insert(data) {
|
||||
const res = this.preinsert(data);
|
||||
res.update(data);
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* @param {Object} data
|
||||
* @returns {Record}
|
||||
*/
|
||||
static preinsert(data) {
|
||||
return this.get(data) ?? this.new(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw relational values of the record, each of which contains object id(s)
|
||||
@@ -377,7 +458,7 @@ export class Record {
|
||||
* @type {Map<string, string|RecordList>}
|
||||
*/
|
||||
__rels__ = new Map();
|
||||
/** Track inverse relations of current record. */
|
||||
/** @type {Map<string, { targetModel: string }>} */
|
||||
__invs__ = new RecordInverses();
|
||||
/** @type {import("@mail/core/common/store_service").Store} */
|
||||
_store;
|
||||
@@ -405,14 +486,12 @@ export class Record {
|
||||
|
||||
setup() {}
|
||||
|
||||
update(data) {}
|
||||
|
||||
delete() {
|
||||
const r1 = this;
|
||||
for (const [name, l1] of r1.__rels__.entries()) {
|
||||
if (l1 instanceof RecordList) {
|
||||
r1[name] = [];
|
||||
} else {
|
||||
r1[name] = undefined;
|
||||
}
|
||||
for (const name of r1.__rels__.keys()) {
|
||||
r1[name] = undefined;
|
||||
}
|
||||
for (const [localId, names] of r1.__invs__.__map__.entries()) {
|
||||
for (const [name2, count] of names.entries()) {
|
||||
|
||||
@@ -6,12 +6,12 @@ import { reactive } from "@odoo/owl";
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { debounce } from "@web/core/utils/timing";
|
||||
import { modelRegistry, Record, RecordInverses, RecordList } from "./record";
|
||||
import { modelRegistry, preinsert, Record, RecordInverses, RecordList } from "./record";
|
||||
|
||||
export class Store extends Record {
|
||||
/** @returns {import("models").Store} */
|
||||
static insert() {
|
||||
return this.get() ?? this.new();
|
||||
return super.insert();
|
||||
}
|
||||
|
||||
/** @type {typeof import("@mail/core/web/activity_model").Activity} */
|
||||
@@ -50,18 +50,10 @@ export class Store extends Record {
|
||||
Thread;
|
||||
|
||||
lastChannelSubscription = "";
|
||||
/**
|
||||
* This is the current logged partner
|
||||
*
|
||||
* @type {import("models").Persona}
|
||||
*/
|
||||
user = null;
|
||||
/**
|
||||
* This is the current logged guest
|
||||
*
|
||||
* @type {import("models").Persona}
|
||||
*/
|
||||
guest = null;
|
||||
/** This is the current logged partner */
|
||||
user = Record.one("Persona");
|
||||
/** This is the current logged guest */
|
||||
guest = Record.one("Persona");
|
||||
/**
|
||||
* The last id of bus notification at the time for fetch init_messaging.
|
||||
* When receiving a notification:
|
||||
@@ -77,8 +69,7 @@ export class Store extends Record {
|
||||
*/
|
||||
inPublicPage = false;
|
||||
companyName = "";
|
||||
/** @type {import("models").Persona} */
|
||||
odoobot = null;
|
||||
odoobot = Record.one("Persona");
|
||||
odoobotOnboarding;
|
||||
users = {};
|
||||
internalUserGroupId = null;
|
||||
@@ -145,9 +136,9 @@ export const storeService = {
|
||||
},
|
||||
};
|
||||
const Models = {};
|
||||
for (const [name, _Model] of modelRegistry.getEntries()) {
|
||||
for (const [name, _OgClass] of modelRegistry.getEntries()) {
|
||||
/** @type {typeof Record} */
|
||||
const Model = _Model;
|
||||
const OgClass = _OgClass;
|
||||
if (res.store[name]) {
|
||||
throw new Error(
|
||||
`There must be no duplicated Model Names (duplicate found: ${name})`
|
||||
@@ -156,27 +147,22 @@ export const storeService = {
|
||||
// classes cannot be made reactive because they are functions and they are not supported.
|
||||
// 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.store });
|
||||
const Model = Object.assign(Object.create(OgClass), { env, store: res.store });
|
||||
// Produce another class with changed prototype, so that there are automatic get/set on relational fields
|
||||
let detecting = true;
|
||||
const Class = {
|
||||
[Model.name]: class extends Model {
|
||||
static __rels__ = new Set();
|
||||
[OgClass.name]: class extends OgClass {
|
||||
constructor() {
|
||||
super();
|
||||
if (detecting) {
|
||||
return;
|
||||
}
|
||||
for (const name of this.constructor.__rels__) {
|
||||
for (const name of Model.__rels__.keys()) {
|
||||
// Relational fields contain symbols for detection in original class.
|
||||
// This constructor is called on genuine records:
|
||||
// - 'one' fields => undefined
|
||||
// - 'many' fields => RecordList
|
||||
let newVal;
|
||||
if (this[name] === Record.one()) {
|
||||
if (this[name]?.[0] === Record.one()[0]) {
|
||||
newVal = undefined;
|
||||
}
|
||||
if (this[name] === Record.many()) {
|
||||
if (this[name]?.[0] === Record.many()[0]) {
|
||||
newVal = new RecordList();
|
||||
newVal.__store__ = res.store;
|
||||
newVal.name = name;
|
||||
@@ -213,63 +199,68 @@ export const storeService = {
|
||||
},
|
||||
/** @param {Record} receiver */
|
||||
set(target, name, val, receiver) {
|
||||
if (receiver.__rels__.has(name)) {
|
||||
const oldVal = receiver.__rels__.get(name);
|
||||
if (oldVal instanceof RecordList) {
|
||||
const r1 = receiver;
|
||||
/** @type {RecordList<Record>} */
|
||||
const l1 = r1.__rels__.get(name);
|
||||
/** @type {Record[]|Set<Record>|RecordList<Record>} */
|
||||
const collection = val;
|
||||
const oldRecords = l1.slice();
|
||||
l1.__list__ = [];
|
||||
for (const r2 of oldRecords) {
|
||||
r2.__invs__.delete(r1.localId, name);
|
||||
}
|
||||
for (const r3 of collection) {
|
||||
if (!receiver.__rels__.has(name)) {
|
||||
Reflect.set(target, name, val, receiver);
|
||||
return true;
|
||||
}
|
||||
const oldVal = receiver.__rels__.get(name);
|
||||
if (oldVal instanceof RecordList) {
|
||||
// [Record.many] =
|
||||
const r1 = receiver;
|
||||
/** @type {RecordList<Record>} */
|
||||
const l1 = r1.__rels__.get(name);
|
||||
/** @type {Record[]|Set<Record>|RecordList<Record>} */
|
||||
const collection = val;
|
||||
const oldRecords = l1.slice();
|
||||
for (const r2 of oldRecords) {
|
||||
r2.__invs__.delete(r1.localId, name);
|
||||
}
|
||||
l1.clear();
|
||||
if ([null, false, undefined].includes(val)) {
|
||||
return true;
|
||||
}
|
||||
for (const v of collection) {
|
||||
preinsert(v, r1, name, (r3) => {
|
||||
l1.__list__.push(r3.localId);
|
||||
r3.__invs__.add(r1.localId, name);
|
||||
}
|
||||
} else {
|
||||
const r1 = receiver;
|
||||
const l1 = r1.__rels__.get(name);
|
||||
const r2 = res.store.get(l1);
|
||||
/** @type {Record} */
|
||||
const r3 = val;
|
||||
if (r2 && r2.notEq(r3)) {
|
||||
r2.__invs__.delete(r1.localId, name);
|
||||
}
|
||||
if (r3) {
|
||||
r1.__rels__.set(name, r3?.localId);
|
||||
if (!(r3 instanceof Record)) {
|
||||
return true; // not a record, ignored
|
||||
}
|
||||
r3.__invs__.add(r1.localId, name);
|
||||
} else {
|
||||
delete r1[name];
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Reflect.set(target, name, val, receiver);
|
||||
// [Record.one] =
|
||||
const r1 = receiver;
|
||||
const l1 = r1.__rels__.get(name);
|
||||
const r2 = res.store.get(l1);
|
||||
if (r2) {
|
||||
r2.__invs__.delete(r1.localId, name);
|
||||
}
|
||||
if ([null, false, undefined].includes(val)) {
|
||||
delete receiver[name];
|
||||
return true;
|
||||
}
|
||||
preinsert(val, r1, name, (r3) => {
|
||||
r1.__rels__.set(name, r3?.localId);
|
||||
});
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
}[Model.name];
|
||||
entry.Class = Class;
|
||||
entry.records = JSON.parse(JSON.stringify(Model.records));
|
||||
Models[name] = entry;
|
||||
res.store[name] = entry;
|
||||
}[OgClass.name];
|
||||
Object.assign(Model, {
|
||||
Class,
|
||||
records: JSON.parse(JSON.stringify(OgClass.records)),
|
||||
__rels__: new Map(),
|
||||
});
|
||||
Models[name] = Model;
|
||||
res.store[name] = Model;
|
||||
// Detect relational fields with a dummy record and setup getter/setters on them
|
||||
const obj = new Model();
|
||||
detecting = false;
|
||||
const obj = new OgClass();
|
||||
for (const [name, val] of Object.entries(obj)) {
|
||||
if (![Record.one(), Record.many()].includes(val)) {
|
||||
if (![Record.one()[0], Record.many()[0]].includes(val?.[0])) {
|
||||
continue;
|
||||
}
|
||||
Class.__rels__.add(name);
|
||||
Model.__rels__.set(name, { targetModel: val[1] });
|
||||
}
|
||||
}
|
||||
// Make true store (as a model)
|
||||
@@ -280,7 +271,7 @@ export const storeService = {
|
||||
res.store[Model.name] = Model;
|
||||
}
|
||||
const store = res.store;
|
||||
store.discuss = store.DiscussApp.insert();
|
||||
store.discuss = {};
|
||||
store.discuss.activeTab = env.services.ui.isSmall ? "mailbox" : "all";
|
||||
onChange(store.Thread, "records", () => store.updateBusSubscription());
|
||||
services.ui.bus.addEventListener("resize", () => {
|
||||
|
||||
@@ -9,15 +9,7 @@ import {
|
||||
useVisible,
|
||||
} from "@mail/utils/common/hooks";
|
||||
|
||||
import {
|
||||
Component,
|
||||
onMounted,
|
||||
onWillStart,
|
||||
onWillUpdateProps,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "@odoo/owl";
|
||||
import { Component, onMounted, onWillUpdateProps, useEffect, useRef, useState } from "@odoo/owl";
|
||||
|
||||
import { Transition } from "@web/core/transition";
|
||||
import { useBus, useService } from "@web/core/utils/hooks";
|
||||
@@ -57,7 +49,11 @@ export class Thread extends Component {
|
||||
setup() {
|
||||
this.escape = escape;
|
||||
this.store = useState(useService("mail.store"));
|
||||
this.state = useState({ isReplyingTo: false, showJumpPresent: false });
|
||||
this.state = useState({
|
||||
isReplyingTo: false,
|
||||
mountedAndLoaded: false,
|
||||
showJumpPresent: false,
|
||||
});
|
||||
this.threadService = useState(useService("mail.thread"));
|
||||
if (!this.env.inChatter || !this.props.hasScrollAdjust) {
|
||||
useAutoScroll("messages", () => {
|
||||
@@ -140,16 +136,23 @@ export class Thread extends Component {
|
||||
},
|
||||
() => [this.props.jumpPresent]
|
||||
);
|
||||
onMounted(() => {
|
||||
this.oldestPersistentMessage = this.props.thread.oldestPersistentMessage?.id;
|
||||
if (!this.env.inChatter || !this.props.hasScrollAdjust) {
|
||||
this.scrollPosition.restore();
|
||||
this.updateShowJumpPresent();
|
||||
}
|
||||
});
|
||||
onWillStart(() => {
|
||||
useEffect(
|
||||
() => {
|
||||
if (!this.state.mountedAndLoaded) {
|
||||
return;
|
||||
}
|
||||
this.oldestPersistentMessage = this.props.thread.oldestPersistentMessage?.id;
|
||||
if (!this.env.inChatter || !this.props.hasScrollAdjust) {
|
||||
this.scrollPosition.restore();
|
||||
this.updateShowJumpPresent();
|
||||
}
|
||||
},
|
||||
() => [this.state.mountedAndLoaded]
|
||||
);
|
||||
onMounted(async () => {
|
||||
this.lastJumpPresent = this.props.jumpPresent;
|
||||
this.threadService.fetchNewMessages(this.props.thread);
|
||||
await this.threadService.fetchNewMessages(this.props.thread);
|
||||
this.state.mountedAndLoaded = true;
|
||||
});
|
||||
useBus(this.env.bus, "MAIL:RELOAD-THREAD", ({ detail }) => {
|
||||
const { model, id } = this.props.thread;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { AND, Record } from "@mail/core/common/record";
|
||||
import { ScrollPosition } from "@mail/core/common/scroll_position";
|
||||
import { replaceArrayWithCompare } from "@mail/utils/common/arrays";
|
||||
import { assignDefined, onChange } from "@mail/utils/common/misc";
|
||||
|
||||
import { deserializeDateTime } from "@web/core/l10n/dates";
|
||||
@@ -27,13 +26,15 @@ export class Thread extends Record {
|
||||
/** @type {Object.<string, import("models").Thread>} */
|
||||
static records = {};
|
||||
/** @returns {import("models").Thread} */
|
||||
static new(data) {
|
||||
return super.new(data);
|
||||
}
|
||||
/** @returns {import("models").Thread} */
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
static new(data) {
|
||||
/** @type {import("models").Thread} */
|
||||
const thread = super.new(data);
|
||||
this.store.Composer.insert({ thread });
|
||||
return thread;
|
||||
}
|
||||
/**
|
||||
* @param {string} localId
|
||||
* @returns {string}
|
||||
@@ -61,7 +62,8 @@ export class Thread extends Record {
|
||||
thread.update(data);
|
||||
return thread;
|
||||
}
|
||||
thread = this.new(data);
|
||||
/** @type {import("models").Thread} */
|
||||
thread = this.preinsert(data);
|
||||
Object.assign(thread, {
|
||||
id: data.id,
|
||||
model: data.model,
|
||||
@@ -76,11 +78,10 @@ export class Thread extends Record {
|
||||
onChange(thread, "channelMembers", () => this.store.updateBusSubscription());
|
||||
onChange(thread, "is_pinned", () => {
|
||||
if (!thread.is_pinned && thread.eq(this.store.discuss.thread)) {
|
||||
delete this.store.discuss.thread;
|
||||
this.store.discuss.thread = undefined;
|
||||
}
|
||||
});
|
||||
thread.update(data);
|
||||
this.store.Composer.insert({ thread });
|
||||
// return reactive version.
|
||||
return thread;
|
||||
}
|
||||
@@ -90,12 +91,7 @@ export class Thread extends Record {
|
||||
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)
|
||||
);
|
||||
this.attachments = attachmentsData.sort((a1, a2) => a2.id - a1.id);
|
||||
}
|
||||
if (serverData) {
|
||||
assignDefined(this, serverData, [
|
||||
@@ -124,9 +120,7 @@ export class Thread extends Record {
|
||||
}
|
||||
const lastServerMessageId = serverData.last_message_id ?? this.lastServerMessage?.id;
|
||||
if (this.lastServerMessage?.id !== lastServerMessageId) {
|
||||
this.lastServerMessage = this._store.Message.insert({
|
||||
id: lastServerMessageId,
|
||||
});
|
||||
this.lastServerMessage = { id: lastServerMessageId };
|
||||
}
|
||||
if (this.model === "discuss.channel" && serverData.channel) {
|
||||
this.channel = assignDefined(this.channel ?? {}, serverData.channel);
|
||||
@@ -165,15 +159,13 @@ export class Thread extends Record {
|
||||
case "ADD":
|
||||
if (members) {
|
||||
for (const member of members) {
|
||||
const record = this._store.ChannelMember.insert(member);
|
||||
this.invitedMembers.add(record);
|
||||
this.invitedMembers.add(member);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "DELETE":
|
||||
for (const member of members) {
|
||||
const record = this._store.ChannelMember.insert(member);
|
||||
this.invitedMembers.delete(record);
|
||||
this.invitedMembers.delete(member);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -183,15 +175,10 @@ export class Thread extends Record {
|
||||
({ fetched_message_id, partner_id, seen_message_id }) => {
|
||||
return {
|
||||
lastFetchedMessage: fetched_message_id
|
||||
? this._store.Message.insert({ id: fetched_message_id })
|
||||
? { 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",
|
||||
}),
|
||||
lastSeenMessage: seen_message_id ? { id: seen_message_id } : undefined,
|
||||
partner: { id: partner_id, type: "partner" },
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -81,7 +81,7 @@ export class ThreadService {
|
||||
* @param {import("models").Thread} thread
|
||||
*/
|
||||
async markAsRead(thread) {
|
||||
if (!thread.isLoaded && thread.status === "loading") {
|
||||
if (!thread.isLoaded) {
|
||||
await thread.isLoadedDeferred;
|
||||
await new Promise(setTimeout);
|
||||
}
|
||||
|
||||
@@ -38,10 +38,6 @@ export class Activity extends Record {
|
||||
/** @type {Object.<number, import("models").Activity>} */
|
||||
static records = {};
|
||||
/** @returns {import("models").Activity} */
|
||||
static new(data) {
|
||||
return super.new(data);
|
||||
}
|
||||
/** @returns {import("models").Activity} */
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
@@ -52,7 +48,8 @@ export class Activity extends Record {
|
||||
* @returns {import("models").Activity}
|
||||
*/
|
||||
static insert(data, { broadcast = true } = {}) {
|
||||
const activity = this.get(data) ?? this.new(data);
|
||||
/** @type {import("models").Activity} */
|
||||
const activity = this.preinsert(data);
|
||||
Object.assign(activity, { id: data.id });
|
||||
if (data.request_partner_id) {
|
||||
data.request_partner_id = data.request_partner_id[0];
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
markup,
|
||||
onMounted,
|
||||
onPatched,
|
||||
onWillStart,
|
||||
onWillUpdateProps,
|
||||
useChildSubEnv,
|
||||
useEffect,
|
||||
@@ -135,9 +134,7 @@ export class Chatter extends Component {
|
||||
"o-mail-Chatter-dropzone"
|
||||
);
|
||||
|
||||
onMounted(this.scrollPosition.restore);
|
||||
onPatched(this.scrollPosition.restore);
|
||||
onWillStart(() => {
|
||||
onMounted(async () => {
|
||||
if (this.props.threadId) {
|
||||
this.state.thread = this.store.Thread.insert({
|
||||
id: this.props.threadId,
|
||||
@@ -145,12 +142,14 @@ export class Chatter extends Component {
|
||||
name: this.props.webRecord?.data?.display_name || undefined,
|
||||
});
|
||||
}
|
||||
return this.load(this.props.threadId, [
|
||||
await this.load(this.props.threadId, [
|
||||
"followers",
|
||||
"attachments",
|
||||
"suggestedRecipients",
|
||||
]);
|
||||
this.scrollPosition.restore();
|
||||
});
|
||||
onPatched(this.scrollPosition.restore);
|
||||
onWillUpdateProps((nextProps) => {
|
||||
this.load(nextProps.threadId, ["followers", "attachments", "suggestedRecipients"]);
|
||||
if (nextProps.threadId === false) {
|
||||
@@ -168,22 +167,33 @@ export class Chatter extends Component {
|
||||
});
|
||||
useEffect(
|
||||
() => {
|
||||
if (this.attachments.length === 0) {
|
||||
if (
|
||||
this.state.thread &&
|
||||
!["new", "loading"].includes(this.state.thread.status) &&
|
||||
this.attachments.length === 0
|
||||
) {
|
||||
this.state.isAttachmentBoxOpened = false;
|
||||
}
|
||||
},
|
||||
() => [this.attachments]
|
||||
() => [this.state.thread?.status, this.attachments]
|
||||
);
|
||||
useEffect(
|
||||
() => {
|
||||
if (this.state.scrollToAttachments > 0) {
|
||||
if (
|
||||
this.state.thread &&
|
||||
!["new", "loading"].includes(this.state.thread.status) &&
|
||||
this.state.scrollToAttachments > 0
|
||||
) {
|
||||
this.attachmentBox.el.scrollIntoView({ block: "center" });
|
||||
}
|
||||
},
|
||||
() => [this.state.scrollToAttachments]
|
||||
() => [this.state.thread?.status, this.state.scrollToAttachments]
|
||||
);
|
||||
useEffect(
|
||||
() => {
|
||||
if (!this.state.thread) {
|
||||
return;
|
||||
}
|
||||
browser.clearTimeout(this.loadingAttachmentTimeout);
|
||||
if (this.state.thread?.isLoadingAttachments) {
|
||||
this.loadingAttachmentTimeout = browser.setTimeout(
|
||||
@@ -195,7 +205,7 @@ export class Chatter extends Component {
|
||||
}
|
||||
return () => browser.clearTimeout(this.loadingAttachmentTimeout);
|
||||
},
|
||||
() => [this.state.thread?.isLoadingAttachments]
|
||||
() => [this.state.thread, this.state.thread?.isLoadingAttachments]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -203,7 +213,7 @@ export class Chatter extends Component {
|
||||
* @returns {import("models").Activity[]}
|
||||
*/
|
||||
get activities() {
|
||||
return this.state.thread.activities;
|
||||
return this.state.thread?.activities ?? [];
|
||||
}
|
||||
|
||||
get followerButtonLabel() {
|
||||
@@ -218,7 +228,7 @@ export class Chatter extends Component {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isDisabled() {
|
||||
return !this.props.threadId || !this.state.thread.hasReadAccess;
|
||||
return !this.props.threadId || !this.state.thread?.hasReadAccess;
|
||||
}
|
||||
|
||||
get attachments() {
|
||||
@@ -229,17 +239,19 @@ export class Chatter extends Component {
|
||||
* @returns {string}
|
||||
*/
|
||||
get toRecipientsText() {
|
||||
const recipients = [...this.state.thread.recipients].slice(0, 5).map(({ partner }) => {
|
||||
const text = partner.email ? partner.emailWithoutDomain : partner.name;
|
||||
return `<span class="text-muted" title="${escapeHTML(partner.email)}">${escapeHTML(
|
||||
text
|
||||
)}</span>`;
|
||||
});
|
||||
const recipients = [...(this.state.thread?.recipients ?? [])]
|
||||
.slice(0, 5)
|
||||
.map(({ partner }) => {
|
||||
const text = partner.email ? partner.emailWithoutDomain : partner.name;
|
||||
return `<span class="text-muted" title="${escapeHTML(partner.email)}">${escapeHTML(
|
||||
text
|
||||
)}</span>`;
|
||||
});
|
||||
const formatter = new Intl.ListFormat(
|
||||
this.store.env.services["user"].lang?.replace("_", "-"),
|
||||
{ type: "unit" }
|
||||
);
|
||||
if (this.state.thread.recipients.length > 5) {
|
||||
if (this.state.thread && this.state.thread.recipients.length > 5) {
|
||||
recipients.push("…");
|
||||
}
|
||||
return markup(formatter.format(recipients));
|
||||
@@ -255,7 +267,7 @@ export class Chatter extends Component {
|
||||
) {
|
||||
const { threadModel } = this.props;
|
||||
this.state.thread = this.threadService.getThread(threadModel, threadId);
|
||||
this.scrollPosition.model = this.state.thread.scrollPosition;
|
||||
this.scrollPosition.model = this.state.thread?.scrollPosition;
|
||||
if (!threadId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.Chatter">
|
||||
<div class="o-mail-Chatter w-100 h-100 flex-grow-1 d-flex flex-column" t-att-class="{ 'overflow-auto': props.hasMessageListScrollAdjust, 'o-chatter-disabled': props.threadId === false }" t-on-scroll="onScrollDebounced" t-ref="root">
|
||||
<div t-if="state.thread" class="o-mail-Chatter w-100 h-100 flex-grow-1 d-flex flex-column" t-att-class="{ 'overflow-auto': props.hasMessageListScrollAdjust, 'o-chatter-disabled': props.threadId === false }" t-on-scroll="onScrollDebounced" t-ref="root">
|
||||
<div class="o-mail-Chatter-top position-sticky top-0" t-att-class="{ 'shadow-sm': state.isTopStickyPinned }" t-ref="top">
|
||||
<div class="o-mail-Chatter-topbar d-flex flex-shrink-0 flex-grow-0 px-3 overflow-x-auto">
|
||||
<button t-if="props.hasMessageList" class="o-mail-Chatter-sendMessage btn text-nowrap me-1" t-att-class="{
|
||||
|
||||
@@ -287,7 +287,7 @@ export class MessagingMenu extends Component {
|
||||
);
|
||||
}
|
||||
if (this.store.discuss.activeTab !== "mailbox") {
|
||||
delete this.store.discuss.thread;
|
||||
this.store.discuss.thread = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,19 +70,14 @@ patch(ThreadService.prototype, {
|
||||
});
|
||||
}
|
||||
if ("mainAttachment" in result) {
|
||||
thread.mainAttachment = result.mainAttachment.id
|
||||
? this.store.Attachment.insert(result.mainAttachment)
|
||||
: undefined;
|
||||
thread.mainAttachment = result.mainAttachment.id ? result.mainAttachment : undefined;
|
||||
}
|
||||
if (!thread.mainAttachment && thread.attachmentsInWebClientView.length > 0) {
|
||||
this.setMainAttachmentFromIndex(thread, 0);
|
||||
}
|
||||
if ("followers" in result) {
|
||||
if (result.selfFollower) {
|
||||
thread.selfFollower = this.store.Follower.insert({
|
||||
followedThread: thread,
|
||||
...result.selfFollower,
|
||||
});
|
||||
thread.selfFollower = { followedThread: thread, ...result.selfFollower };
|
||||
}
|
||||
thread.followersCount = result.followersCount;
|
||||
for (const followerData of result.followers) {
|
||||
@@ -96,11 +91,7 @@ patch(ThreadService.prototype, {
|
||||
}
|
||||
thread.recipientsCount = result.recipientsCount;
|
||||
for (const recipientData of result.recipients) {
|
||||
const recipient = this.store.Follower.insert({
|
||||
followedThread: thread,
|
||||
...recipientData,
|
||||
});
|
||||
thread.recipients.add(recipient);
|
||||
thread.recipients.add({ followedThread: thread, ...recipientData });
|
||||
}
|
||||
}
|
||||
if ("suggestedRecipients" in result) {
|
||||
@@ -123,18 +114,15 @@ patch(ThreadService.prototype, {
|
||||
type: "chatter",
|
||||
});
|
||||
if (resId === false) {
|
||||
const tmpId = this.messageService.getNextTemporaryId();
|
||||
const tmpData = {
|
||||
id: tmpId,
|
||||
thread.messages.push({
|
||||
id: this.messageService.getNextTemporaryId(),
|
||||
author: { id: this.store.self.id },
|
||||
body: _t("Creating a new record..."),
|
||||
message_type: "notification",
|
||||
trackingValues: [],
|
||||
res_id: thread.id,
|
||||
model: thread.model,
|
||||
};
|
||||
const message = this.store.Message.insert(tmpData);
|
||||
thread.messages.push(message);
|
||||
});
|
||||
}
|
||||
return thread;
|
||||
},
|
||||
@@ -156,12 +144,7 @@ patch(ThreadService.prototype, {
|
||||
email,
|
||||
lang,
|
||||
reason,
|
||||
persona: partner_id
|
||||
? this.store.Persona.insert({
|
||||
type: "partner",
|
||||
id: partner_id,
|
||||
})
|
||||
: false,
|
||||
persona: partner_id ? { type: "partner", id: partner_id } : false,
|
||||
checked: true,
|
||||
});
|
||||
}
|
||||
@@ -198,11 +181,7 @@ patch(ThreadService.prototype, {
|
||||
{ filter_recipients: true }
|
||||
);
|
||||
for (const data of recipients) {
|
||||
const recipient = this.store.Follower.insert({
|
||||
followedThread: thread,
|
||||
...data,
|
||||
});
|
||||
thread.recipients.add(recipient);
|
||||
thread.recipients.add({ followedThread: thread, ...data });
|
||||
}
|
||||
},
|
||||
open(thread, replaceNewMessageChatWindow) {
|
||||
|
||||
@@ -273,7 +273,7 @@ export class Rtc {
|
||||
* @param {import("models").Thread} [channel]
|
||||
*/
|
||||
endCall(channel = this.state.channel) {
|
||||
channel.rtcInvitingSessionId = undefined;
|
||||
channel.rtcInvitingSession = undefined;
|
||||
if (channel.eq(this.state.channel)) {
|
||||
this.clear();
|
||||
this.soundEffectsService.play("channel-leave");
|
||||
@@ -762,7 +762,7 @@ export class Rtc {
|
||||
3000,
|
||||
true
|
||||
);
|
||||
this.state.channel.rtcInvitingSessionId = undefined;
|
||||
this.state.channel.rtcInvitingSession = undefined;
|
||||
this.call();
|
||||
this.soundEffectsService.play("channel-join");
|
||||
await this.resetAudioTrack({ force: true });
|
||||
@@ -830,8 +830,7 @@ export class Rtc {
|
||||
if (this.state.channel && rtcSessions) {
|
||||
const activeSessionsData = rtcSessions[0][1];
|
||||
for (const sessionData of activeSessionsData) {
|
||||
const session = this.store.RtcSession.insert(sessionData);
|
||||
this.state.channel.rtcSessions.add(session);
|
||||
this.state.channel.rtcSessions.add(sessionData);
|
||||
}
|
||||
const outdatedSessionsData = rtcSessions[1][1];
|
||||
for (const sessionData of outdatedSessionsData) {
|
||||
@@ -892,19 +891,19 @@ export class Rtc {
|
||||
// ignore error during remove, the value will be overwritten at next usage anyway
|
||||
}
|
||||
}
|
||||
delete session.audioStream;
|
||||
delete session.connectionState;
|
||||
delete session.localCandidateType;
|
||||
delete session.remoteCandidateType;
|
||||
delete session.dataChannelState;
|
||||
delete session.packetsReceived;
|
||||
delete session.packetsSent;
|
||||
delete session.dtlsState;
|
||||
delete session.iceState;
|
||||
delete session.raisingHand;
|
||||
delete session.logStep;
|
||||
delete session.audioError;
|
||||
delete session.videoError;
|
||||
session.audioStream = undefined;
|
||||
session.connectionState = undefined;
|
||||
session.localCandidateType = undefined;
|
||||
session.remoteCandidateType = undefined;
|
||||
session.dataChannelState = undefined;
|
||||
session.packetsReceived = undefined;
|
||||
session.packetsSent = undefined;
|
||||
session.dtlsState = undefined;
|
||||
session.iceState = undefined;
|
||||
session.raisingHand = undefined;
|
||||
session.logStep = undefined;
|
||||
session.audioError = undefined;
|
||||
session.videoError = undefined;
|
||||
session.isTalking = false;
|
||||
this.removeVideoFromSession(session);
|
||||
session.dataChannel?.close();
|
||||
@@ -927,7 +926,7 @@ export class Rtc {
|
||||
}
|
||||
}
|
||||
peerConnection.close();
|
||||
delete session.peerConnection;
|
||||
session.peerConnection = undefined;
|
||||
}
|
||||
browser.clearTimeout(this.state.recoverTimeouts.get(session.id));
|
||||
this.state.recoverTimeouts.delete(session.id);
|
||||
@@ -1537,8 +1536,7 @@ export class Rtc {
|
||||
break;
|
||||
case "ADD":
|
||||
for (const sessionData of sessionsData) {
|
||||
const session = this.store.RtcSession.insert(sessionData);
|
||||
channel.rtcSessions.add(session);
|
||||
channel.rtcSessions.add(sessionData);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,6 @@ export class RtcSession extends Record {
|
||||
/** @type {Object.<number, import("models").RtcSession>} */
|
||||
static records = {};
|
||||
/** @returns {import("models").RtcSession} */
|
||||
static new(data) {
|
||||
return super.new(data);
|
||||
}
|
||||
/** @returns {import("models").RtcSession} */
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
@@ -19,7 +15,8 @@ export class RtcSession extends Record {
|
||||
* @returns {number, import("models").RtcSession}
|
||||
*/
|
||||
static insert(data) {
|
||||
const session = this.get(data) ?? this.new(data);
|
||||
/** @type {import("models").RtcSession} */
|
||||
const session = this.preinsert(data);
|
||||
const { channelMember, ...remainingData } = data;
|
||||
for (const key in remainingData) {
|
||||
session[key] = remainingData[key];
|
||||
|
||||
@@ -8,8 +8,7 @@ patch(Thread.prototype, {
|
||||
update(data) {
|
||||
super.update(data);
|
||||
if ("rtc_inviting_session" in data) {
|
||||
const session = this._store.RtcSession.insert(data.rtc_inviting_session);
|
||||
this.rtcSessions.add(session);
|
||||
this.rtcSessions.add(data.rtc_inviting_session);
|
||||
this._store.discuss.ringingThreads.add(this);
|
||||
}
|
||||
let rtcContinue = true;
|
||||
@@ -21,8 +20,7 @@ patch(Thread.prototype, {
|
||||
}
|
||||
rtcContinue = false;
|
||||
} else {
|
||||
const session = this._store.RtcSession.insert(data.rtcInvitingSession);
|
||||
this.rtcSessions.add(session);
|
||||
this.rtcSessions.add(data.rtcInvitingSession);
|
||||
this._store.discuss.ringingThreads.add(this);
|
||||
}
|
||||
}
|
||||
@@ -37,8 +35,7 @@ patch(Thread.prototype, {
|
||||
break;
|
||||
case "ADD":
|
||||
for (const rtcSessionData of sessionsData) {
|
||||
const session = this._store.RtcSession.insert(rtcSessionData);
|
||||
this.rtcSessions.add(session);
|
||||
this.rtcSessions.add(rtcSessionData);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ patch(MessageModel.prototype, {
|
||||
this.originThread.pinnedMessages.add(this);
|
||||
this.pinnedAt = pinnedAt;
|
||||
} else {
|
||||
delete this.pinnedAt;
|
||||
this.pinnedAt = undefined;
|
||||
this.originThread.pinnedMessages.delete(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { toRaw } from "@odoo/owl";
|
||||
|
||||
export function removeFromArray(array, elem) {
|
||||
const index = array.indexOf(elem);
|
||||
if (index >= 0) {
|
||||
@@ -15,29 +13,3 @@ export function removeFromArrayWithPredicate(array, predicate) {
|
||||
array.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the content of array1 with the content of array2. Order of elements
|
||||
* is not guaranteed: new elements are inserted last.
|
||||
*
|
||||
* Smart process to avoid triggering reactives when there is no change between
|
||||
* the 2 arrays.
|
||||
*/
|
||||
export function replaceArrayWithCompare(array1, array2) {
|
||||
array1 = toRaw(array1);
|
||||
array2 = toRaw(array2);
|
||||
const elementsToRemove = new Set();
|
||||
for (const el1 of array1) {
|
||||
if (!array2.includes(el1)) {
|
||||
elementsToRemove.add(el1);
|
||||
}
|
||||
}
|
||||
for (const el of elementsToRemove) {
|
||||
removeFromArray(array1, el);
|
||||
}
|
||||
for (const el2 of array2) {
|
||||
if (!array1.includes(el2)) {
|
||||
array1.push(el2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,6 +331,7 @@ export function useSelection({ refName, model, preserveOnClickAwayPredicate = ()
|
||||
*/
|
||||
export function useScrollPosition(refName, model, clearOn) {
|
||||
const ref = useRef(refName);
|
||||
let observeScroll = false;
|
||||
const self = {
|
||||
ref,
|
||||
model,
|
||||
@@ -368,11 +369,21 @@ export function useScrollPosition(refName, model, clearOn) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
ref.el.addEventListener("scroll", onScrolled);
|
||||
if (ref.el) {
|
||||
observeScroll = true;
|
||||
}
|
||||
ref.el?.addEventListener("scroll", onScrolled);
|
||||
});
|
||||
|
||||
onPatched(() => {
|
||||
if (!observeScroll && ref.el) {
|
||||
observeScroll = true;
|
||||
ref.el.addEventListener("scroll", onScrolled);
|
||||
}
|
||||
});
|
||||
|
||||
onWillUnmount(() => {
|
||||
ref.el.removeEventListener("scroll", onScrolled);
|
||||
ref.el?.removeEventListener("scroll", onScrolled);
|
||||
});
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -612,7 +612,8 @@ QUnit.test("emptying inbox displays rainbow man in inbox", async () => {
|
||||
]);
|
||||
const { openDiscuss } = await start();
|
||||
openDiscuss();
|
||||
await click("button", { text: "Mark all read" });
|
||||
await contains(".o-mail-Message");
|
||||
await click("button:enabled", { text: "Mark all read" });
|
||||
await contains(".o_reward_rainbow");
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { startServer } from "@bus/../tests/helpers/mock_python_environment";
|
||||
|
||||
import { start } from "@mail/../tests/helpers/test_utils";
|
||||
|
||||
import { getOrigin } from "@web/core/utils/urls";
|
||||
import { patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { click, contains } from "@web/../tests/utils";
|
||||
|
||||
@@ -87,10 +88,9 @@ QUnit.test("reply shows correct author avatar", async (assert) => {
|
||||
});
|
||||
const { openDiscuss } = await start();
|
||||
await openDiscuss(channelId);
|
||||
const replyAvatar = document.querySelector(".o-mail-MessageInReply-avatar");
|
||||
assert.ok(
|
||||
replyAvatar.dataset["src"].includes(
|
||||
`/discuss/channel/${channelId}/partner/${pyEnv.currentPartnerId}/avatar_128`
|
||||
)
|
||||
await contains(
|
||||
`.o-mail-MessageInReply-avatar[data-src='${`${getOrigin()}/discuss/channel/${channelId}/partner/${
|
||||
pyEnv.currentPartnerId
|
||||
}/avatar_128`}']`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -16,7 +16,10 @@ QUnit.test("Form view not scrolled when switching record", async () => {
|
||||
description: [...Array(60).keys()].join("\n"),
|
||||
display_name: "Partner 1",
|
||||
},
|
||||
{ display_name: "Partner 2" },
|
||||
{
|
||||
description: [...Array(60).keys()].join("\n"),
|
||||
display_name: "Partner 2",
|
||||
},
|
||||
]);
|
||||
const messages = [...Array(60).keys()].map((id) => {
|
||||
return {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { startServer } from "@bus/../tests/helpers/mock_python_environment";
|
||||
|
||||
import { start } from "@mail/../tests/helpers/test_utils";
|
||||
|
||||
import { contains } from "@web/../tests/utils";
|
||||
|
||||
QUnit.module("chatter");
|
||||
|
||||
QUnit.test("Send message button activation (access rights dependent)", async function (assert) {
|
||||
@@ -54,19 +56,10 @@ QUnit.test("Send message button activation (access rights dependent)", async fun
|
||||
res_model: model,
|
||||
views: [[false, "form"]],
|
||||
});
|
||||
const details = `hasReadAccess: ${hasReadAccess}, hasWriteAccess: ${hasWriteAccess}, model: ${model}, resId: ${resId}`;
|
||||
if (enabled) {
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".o-mail-Chatter-topbar button:contains(Send message):disabled",
|
||||
`${msg}: send message button must not be disabled (${details}`
|
||||
);
|
||||
await contains(".o-mail-Chatter-topbar button:enabled", { text: "Send message" });
|
||||
} else {
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".o-mail-Chatter-topbar button:contains(Send message):disabled",
|
||||
`${msg}: send message button must be disabled (${details})`
|
||||
);
|
||||
await contains(".o-mail-Chatter-topbar button:disabled", { text: "Send message" });
|
||||
}
|
||||
}
|
||||
await assertSendButton(
|
||||
|
||||
@@ -16,6 +16,7 @@ import { deepCopy, pick } from "@web/core/utils/objects";
|
||||
import { makeFakeRPCService, makeMockFetch } from "./mock_services";
|
||||
import { patchWithCleanup } from "./utils";
|
||||
import { makeErrorFromResponse } from "@web/core/network/rpc_service";
|
||||
import { registerCleanup } from "./cleanup";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
@@ -134,6 +135,7 @@ function traverseElementTree(tree, cb) {
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export class MockServer {
|
||||
active = true;
|
||||
constructor(data, options = {}) {
|
||||
this.init(data, options);
|
||||
}
|
||||
@@ -2517,6 +2519,10 @@ export async function makeMockServer(serverData, mockRPC) {
|
||||
// simulates that we serialized the call to be passed in a real request
|
||||
args = JSON.parse(JSON.stringify(args));
|
||||
}
|
||||
if (!mockServer.active) {
|
||||
// End of test => all RPCs are blocking
|
||||
return new Promise(() => {});
|
||||
}
|
||||
if (mockRPC) {
|
||||
res = await mockRPC(route, args, mockServer.performRPC.bind(mockServer));
|
||||
}
|
||||
@@ -2558,6 +2564,7 @@ export async function makeMockServer(serverData, mockRPC) {
|
||||
});
|
||||
}
|
||||
// Replace RPC service
|
||||
registerCleanup(() => (mockServer.active = false));
|
||||
serviceRegistry.add("rpc", rpcService, { force: true });
|
||||
return mockServer;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
declare module "models" {
|
||||
export interface Thread {
|
||||
visitor: Persona,
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { Record } from "@mail/core/common/record";
|
||||
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, {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.visitor = Record.one("Persona");
|
||||
},
|
||||
update(data) {
|
||||
super.update(data);
|
||||
if (data?.visitor) {
|
||||
this.visitor = this._store.Persona.insert({
|
||||
...data.visitor,
|
||||
type: "visitor",
|
||||
});
|
||||
this.visitor = { ...data.visitor, type: "visitor" };
|
||||
}
|
||||
assignDefined(this, data, ["requested_by_operator"]);
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ import { startServer } from "@bus/../tests/helpers/mock_python_environment";
|
||||
|
||||
import { start } from "@mail/../tests/helpers/test_utils";
|
||||
|
||||
import { click } from "@web/../tests/utils";
|
||||
import { click, contains } from "@web/../tests/utils";
|
||||
|
||||
QUnit.module("activity (patch)");
|
||||
|
||||
@@ -36,9 +36,7 @@ QUnit.test("grant course access", async (assert) => {
|
||||
res_model: "slide.channel",
|
||||
views: [[false, "form"]],
|
||||
});
|
||||
assert.containsOnce($, ".o-mail-Activity");
|
||||
assert.containsOnce($, "button:contains(Grant Access)");
|
||||
|
||||
await contains(".o-mail-Activity");
|
||||
await click("button", { text: "Grant Access" });
|
||||
assert.verifySteps(["access_grant"]);
|
||||
});
|
||||
@@ -71,9 +69,7 @@ QUnit.test("refuse course access", async (assert) => {
|
||||
res_model: "slide.channel",
|
||||
views: [[false, "form"]],
|
||||
});
|
||||
assert.containsOnce($, ".o-mail-Activity");
|
||||
assert.containsOnce($, "button:contains(Refuse Access)");
|
||||
|
||||
await contains(".o-mail-Activity");
|
||||
await click("button", { text: "Refuse Access" });
|
||||
assert.verifySteps(["access_refuse"]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user