[REF] mail, *: turn store.discuss into a model

This allows to define categories as also models, which allow
to register list of threads as relational fields, which in turn
simplify code readability and maintainability of these relational
data.

Part-of: odoo/odoo#136308
This commit is contained in:
Alexandre Kühn
2023-09-27 13:11:58 +00:00
parent 603cd70ecc
commit d97ffb0dde
22 changed files with 186 additions and 138 deletions
@@ -30,11 +30,9 @@ patch(Composer.prototype, {
return (
this.thread?.type === "livechat" &&
!this.env.inChatWindow &&
Object.values(this.store.discuss.livechat.threads).some((localId) => {
return (
localId !== this.thread.localId && this.store.Thread.records[localId].isUnread
);
})
this.store.discuss.livechat.threads.some(
(thread) => thread.notEq(this.thread) && thread.isUnread
)
);
},
});
+5
View File
@@ -0,0 +1,5 @@
declare module "models" {
export interface DiscussApp {
livechat: DiscussAppCategory,
}
}
@@ -1,14 +1,15 @@
/* @odoo-module */
import { Store } from "@mail/core/common/store_service";
import { DiscussApp } from "@mail/core/common/discuss_app_model";
import { Record } from "@mail/core/common/record";
import { _t } from "@web/core/l10n/translation";
import { patch } from "@web/core/utils/patch";
patch(Store.prototype, {
setup(env) {
super.setup(env);
this.discuss.livechat = {
patch(DiscussApp, {
new(data) {
const res = super.new(data);
res.livechat = this.store.DiscussAppCategory.insert({
extraClass: "o-mail-DiscussSidebarCategory-livechat",
id: "livechat",
name: _t("Livechat"),
@@ -16,7 +17,14 @@ patch(Store.prototype, {
canView: false,
canAdd: false,
serverStateKey: "is_discuss_sidebar_category_livechat_open",
threads: [], // list of ids
};
});
return res;
},
});
patch(DiscussApp.prototype, {
setup(env) {
super.setup(env);
this.livechat = Record.one("DiscussAppCategory");
},
});
@@ -22,7 +22,7 @@ patch(Thread, {
});
}
if (isUnknown) {
this.store.discuss.livechat.threads.push(thread.localId);
this.store.discuss.livechat.threads.push(thread);
this.env.services["mail.thread"].sortChannels();
}
}
@@ -2,7 +2,6 @@
import { DEFAULT_AVATAR } from "@mail/core/common/persona_service";
import { ThreadService } from "@mail/core/common/thread_service";
import { removeFromArray } from "@mail/utils/common/arrays";
import { patch } from "@web/core/utils/patch";
@@ -18,12 +17,6 @@ patch(ThreadService.prototype, {
this.store.discuss.activeTab = "livechat";
}
},
remove(thread) {
if (thread.type === "livechat") {
removeFromArray(this.store.discuss.livechat.threads, thread.localId);
}
super.remove(thread);
},
canLeave(thread) {
return thread.type !== "livechat" && super.canLeave(thread);
@@ -46,27 +39,18 @@ patch(ThreadService.prototype, {
sortChannels() {
super.sortChannels();
// Live chats are sorted by most recent interest date time in the sidebar.
this.store.discuss.livechat.threads.sort((localId_1, localId_2) => {
const thread1 = this.store.Thread.records[localId_1];
const thread2 = this.store.Thread.records[localId_2];
return thread2.lastInterestDateTime?.ts - thread1.lastInterestDateTime?.ts;
});
this.store.discuss.livechat.threads.sort(
(t1, t2) => t2.lastInterestDateTime?.ts - t1.lastInterestDateTime?.ts
);
},
/**
* @returns {boolean} Whether the livechat thread changed.
*/
goToOldestUnreadLivechatThread() {
const oldestUnreadThread =
this.store.Thread.records[
Object.values(this.store.discuss.livechat.threads)
.filter((localId) => this.store.Thread.records[localId].isUnread)
.sort(
(localId_1, localId_2) =>
this.store.Thread.records[localId_1].lastInterestDateTime?.ts -
this.store.Thread.records[localId_2].lastInterestDateTime?.ts
)[0]
];
const oldestUnreadThread = this.store.discuss.livechat.threads
.filter((thread) => thread.isUnread)
.sort((t1, t2) => t1.lastInterestDateTime?.ts - t2.lastInterestDateTime?.ts)[0];
if (!oldestUnreadThread) {
return false;
}
@@ -5,10 +5,7 @@ import { discussSidebarCategoriesRegistry } from "@mail/discuss/core/web/discuss
discussSidebarCategoriesRegistry.add(
"livechats",
{
predicate: (store) =>
store.discuss.livechat.threads.some(
(localId) => store.Thread.records[localId]?.is_pinned
),
predicate: (store) => store.discuss.livechat.threads.some((thread) => thread?.is_pinned),
value: (store) => store.discuss.livechat,
},
{ sequence: 20 }
@@ -157,7 +157,7 @@ export class LivechatService {
(c) => c.thread.id === this.TEMPORARY_ID
);
if (chatWindow) {
this.env.services["mail.thread"].remove(chatWindow.thread);
chatWindow.thread?.delete();
if (!this.thread) {
this.chatWindowService.close(chatWindow);
return;
+6
View File
@@ -3,6 +3,8 @@ declare module "models" {
import { CannedResponse as CannedResponseClass } from "@mail/core/common/canned_response_model";
import { ChannelMember as ChannelMemberClass } from "@mail/core/common/channel_member_model";
import { Composer as ComposerClass } from "@mail/core/common/composer_model";
import { DiscussApp as DiscussAppClass } from "@mail/core/common/discuss_app_model";
import { DiscussAppCategory as DiscussAppCategoryClass } from "@mail/core/common/discuss_app_category_model";
import { Follower as FollowerClass } from "@mail/core/common/follower_model";
import { LinkPreview as LinkPreviewClass } from "@mail/core/common/link_preview_model";
import { Message as MessageClass } from "@mail/core/common/message_model";
@@ -16,6 +18,8 @@ declare module "models" {
export interface Attachment extends CannedResponseClass {}
export interface ChannelMember extends ChannelMemberClass {}
export interface Composer extends ComposerClass {}
export interface DiscussApp extends DiscussAppClass {}
export interface DiscussAppCategory extends DiscussAppCategoryClass {}
export interface Follower extends FollowerClass {}
export interface LinkPreview extends LinkPreviewClass {}
export interface Message extends MessageClass {}
@@ -30,6 +34,8 @@ declare module "models" {
"CannedResponse": CannedResponse,
"ChannelMember": ChannelMember,
"Composer": Composer,
"DiscussApp": DiscussApp,
"DiscussAppCategory": DiscussAppCategory,
"Follower": Follower,
"LinkPreview": LinkPreview,
"Message": Message,
@@ -83,7 +83,7 @@ export class Discuss extends Component {
}
get thread() {
return this.store.Thread.records[this.store.discuss.threadLocalId];
return this.store.discuss.thread;
}
async onFileUploaded(file) {
@@ -93,7 +93,7 @@
<t t-name="mail.MobileMailbox">
<button class="btn btn-secondary flex-grow-1 p-2"
t-att-class="{
'active o-active shadow-none': store.discuss.threadLocalId === mailbox.localId,
'active o-active shadow-none': mailbox.eq(store.discuss.thread),
}" t-on-click="() => this.threadService.setDiscussThread(mailbox)" t-esc="mailbox.name"
/>
</t>
@@ -0,0 +1,41 @@
/* @odoo-module */
import { assignDefined } from "@mail/utils/common/misc";
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);
assignDefined(category, data);
return category;
}
/** @type {string} */
extraClass;
/** @string */
id;
/** @type {string} */
name;
isOpen = false;
canView = false;
canAdd = false;
/** @type {string} */
serverStateKey;
/** @type {string} */
addTitle;
/** @type {string} */
addHotkey;
threads = Record.many("Thread");
}
DiscussAppCategory.register();
@@ -0,0 +1,58 @@
/* @odoo-module */
import { _t } from "@web/core/l10n/translation";
import { Record } from "./record";
export class DiscussApp extends Record {
/** @returns {import("models").DiscussApp} */
static new(data) {
const res = super.new(data);
Object.assign(res, {
channels: this.store.DiscussAppCategory.insert({
extraClass: "o-mail-DiscussSidebarCategory-channel",
id: "channels",
name: _t("Channels"),
isOpen: false,
canView: true,
canAdd: true,
serverStateKey: "is_discuss_sidebar_category_channel_open",
addTitle: _t("Add or join a channel"),
addHotkey: "c",
}),
chats: this.store.DiscussAppCategory.insert({
extraClass: "o-mail-DiscussSidebarCategory-chat",
id: "chats",
name: _t("Direct messages"),
isOpen: false,
canView: false,
canAdd: true,
serverStateKey: "is_discuss_sidebar_category_chat_open",
addTitle: _t("Start a conversation"),
addHotkey: "d",
}),
});
return res;
}
/** @returns {import("models").DiscussApp} */
static get(data) {
return super.get(data);
}
/** @returns {import("models").DiscussApp} */
static insert() {
const app = this.get() ?? this.new();
return app;
}
/** @type {'mailbox'|'all'|'channel'|'chat'|'livechat'} */
activeTab = "all";
isActive = false;
thread = Record.one("Thread");
channels = Record.one("DiscussAppCategory");
chats = Record.one("DiscussAppCategory");
// mailboxes in sidebar
inbox = Record.one("Thread");
starred = Record.one("Thread");
history = Record.one("Thread");
}
DiscussApp.register();
@@ -214,6 +214,10 @@ export class RecordList extends Array {
includes(record) {
return this.__list__.includes(record.localId);
}
/** @param {(acc: any, r: R) => any} fn */
reduce(fn, init) {
return this.__list__.reduce((acc, localId) => fn(acc, this.__store__.get(localId)), init);
}
/**
* @param {number} [start]
* @param {number} [end]
@@ -4,7 +4,6 @@ import { onChange } from "@mail/utils/common/misc";
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, Record, RecordInverses, RecordList } from "./record";
@@ -22,6 +21,10 @@ export class Store {
ChatWindow;
/** @type {typeof import("@mail/core/common/composer_model").Composer} */
Composer;
/** @type {typeof import("@mail/core/common/discuss_app_model").DiscussApp} */
DiscussApp;
/** @type {typeof import("@mail/core/common/discuss_app_category_model").DiscussAppCategory} */
DiscussAppCategory;
/** @type {typeof import("@mail/core/common/follower_model").Follower} */
Follower;
/** @type {typeof import("@mail/core/common/link_preview_model").LinkPreview} */
@@ -70,7 +73,6 @@ export class Store {
*/
setup(env) {
this.env = env;
this.discuss.activeTab = this.env.services.ui.isSmall ? "mailbox" : "all";
}
updateBusSubscription() {
@@ -141,43 +143,7 @@ export class Store {
counter: 0,
};
// discuss app
discuss = {
activeTab: "all", // can be 'mailbox', 'all', 'channel' or 'chat'
isActive: false,
threadLocalId: null,
channels: {
extraClass: "o-mail-DiscussSidebarCategory-channel",
id: "channels",
name: _t("Channels"),
isOpen: false,
canView: true,
canAdd: true,
serverStateKey: "is_discuss_sidebar_category_channel_open",
addTitle: _t("Add or join a channel"),
addHotkey: "c",
threads: [], // list of ids
},
chats: {
extraClass: "o-mail-DiscussSidebarCategory-chat",
id: "chats",
name: _t("Direct messages"),
isOpen: false,
canView: false,
canAdd: true,
serverStateKey: "is_discuss_sidebar_category_chat_open",
addTitle: _t("Start a conversation"),
addHotkey: "d",
threads: [], // list of ids
},
// mailboxes in sidebar
/** @type {import("models").Thread} */
inbox: null,
/** @type {import("models").Thread} */
starred: null,
/** @type {import("models").Thread} */
history: null,
};
discuss = Record.one("DiscussApp");
activityCounter = 0;
@@ -245,7 +211,7 @@ export const storeService = {
}
return Reflect.get(target, name, receiver);
},
deleteProperty(target, key) {
deleteProperty(target, name) {
if (name !== "__rels__" && target.__rels__.has(name)) {
const r1 = target;
const l1 = r1.__rels__.get(name);
@@ -253,8 +219,9 @@ export const storeService = {
if (r2) {
r2.__invs__.delete(r1.localId, name);
}
r1.__rels__.set(name, undefined);
}
const ret = Reflect.deleteProperty(target, key);
const ret = Reflect.deleteProperty(target, name);
return ret;
},
/** @param {Record} receiver */
@@ -315,13 +282,14 @@ export const storeService = {
Class.__rels__.add(name);
}
}
res.discuss = res.DiscussApp.insert();
res.discuss.activeTab = env.services.ui.isSmall ? "mailbox" : "all";
onChange(res.Thread, "records", () => res.updateBusSubscription());
services.ui.bus.addEventListener("resize", () => {
if (!services.ui.isSmall) {
res.discuss.activeTab = "all";
} else {
res.discuss.activeTab =
res.Thread.records[res.discuss.threadLocalId]?.type ?? "all";
res.discuss.activeTab = res.discuss.thread?.type ?? "all";
}
});
return res;
@@ -75,8 +75,8 @@ export class Thread extends Record {
onChange(thread, "isLoaded", () => thread.isLoadedDeferred.resolve());
onChange(thread, "channelMembers", () => this.store.updateBusSubscription());
onChange(thread, "is_pinned", () => {
if (!thread.is_pinned && this.store.discuss.threadLocalId === thread.localId) {
this.store.discuss.threadLocalId = null;
if (!thread.is_pinned && thread.eq(this.store.discuss.thread)) {
delete this.store.discuss.thread;
}
});
thread.update(data);
@@ -199,16 +199,13 @@ export class Thread extends Record {
);
}
}
if (
this.type === "channel" &&
!this._store.discuss.channels.threads.includes(this.localId)
) {
this._store.discuss.channels.threads.push(this.localId);
if (this.type === "channel" && !this._store.discuss.channels.threads.includes(this)) {
this._store.discuss.channels.threads.push(this);
} else if (
(this.type === "chat" || this.type === "group") &&
!this._store.discuss.chats.threads.includes(this.localId)
!this._store.discuss.chats.threads.includes(this)
) {
this._store.discuss.chats.threads.push(this.localId);
this._store.discuss.chats.threads.push(this);
}
if (!this.type && !["mail.box", "discuss.channel"].includes(this.model)) {
this.type = "chatter";
@@ -2,7 +2,6 @@
import { loadEmoji } from "@web/core/emoji_picker/emoji_picker";
import { DEFAULT_AVATAR } from "@mail/core/common/persona_service";
import { removeFromArray } from "@mail/utils/common/arrays";
import { prettifyMessageContent } from "@mail/utils/common/format";
import { markup } from "@odoo/owl";
@@ -413,7 +412,7 @@ export class ThreadService {
}
unpin(thread) {
if (this.store.discuss.threadLocalId === thread.localId) {
if (thread.eq(this.store.discuss.thread)) {
this.router.replaceState({ active_id: undefined });
}
if (thread.model !== "discuss.channel") {
@@ -435,16 +434,12 @@ export class ThreadService {
}
sortChannels() {
this.store.discuss.channels.threads.sort((id1, id2) => {
const thread1 = this.store.Thread.records[id1];
const thread2 = this.store.Thread.records[id2];
return String.prototype.localeCompare.call(thread1.name, thread2.name);
});
this.store.discuss.chats.threads.sort((localId_1, localId_2) => {
const thread1 = this.store.Thread.records[localId_1];
const thread2 = this.store.Thread.records[localId_2];
return thread2.lastInterestDateTime.ts - thread1.lastInterestDateTime.ts;
});
this.store.discuss.channels.threads.sort((t1, t2) =>
String.prototype.localeCompare.call(t1.name, t2.name)
);
this.store.discuss.chats.threads.sort(
(t1, t2) => t2.lastInterestDateTime.ts - t1.lastInterestDateTime.ts
);
}
/**
@@ -626,10 +621,10 @@ export class ThreadService {
async leaveChannel(channel) {
await this.orm.call("discuss.channel", "action_unfollow", [channel.id]);
this.remove(channel);
channel.delete();
this.setDiscussThread(
this.store.discuss.channels.threads[0]
? this.store.Thread.records[this.store.discuss.channels.threads[0]]
? this.store.discuss.channels.threads[0]
: this.store.discuss.inbox
);
}
@@ -639,7 +634,7 @@ export class ThreadService {
* @param {boolean} pushState
*/
setDiscussThread(thread, pushState = true) {
this.store.discuss.threadLocalId = thread.localId;
this.store.discuss.thread = thread;
const activeId =
typeof thread.id === "string"
? `mail.box_${thread.id}`
@@ -656,12 +651,6 @@ export class ThreadService {
}
}
remove(thread) {
removeFromArray(this.store.discuss.chats.threads, thread.localId);
removeFromArray(this.store.discuss.channels.threads, thread.localId);
thread.delete();
}
/**
* @param {import("models").Thread} thread
* @param {string} body
@@ -835,8 +824,7 @@ export class ThreadService {
}
getDiscussSidebarCategoryCounter(categoryId) {
return this.store.discuss[categoryId].threads.reduce((acc, threadLocalId) => {
const channel = this.store.Thread.records[threadLocalId];
return this.store.discuss[categoryId].threads.reduce((acc, channel) => {
if (categoryId === "channels") {
return channel.message_needaction_counter > 0 ? acc + 1 : acc;
} else {
@@ -44,7 +44,7 @@ export class DiscussClientAction extends Component {
const rawActiveId =
props.action.context.active_id ??
props.action.params?.active_id ??
this.store.Thread.localIdToActiveId(this.store.discuss.threadLocalId) ??
this.store.Thread.localIdToActiveId(this.store.discuss.thread?.localId) ??
"mail.box_inbox";
const activeId =
typeof rawActiveId === "number" ? `discuss.channel_${rawActiveId}` : rawActiveId;
@@ -53,8 +53,8 @@ export class DiscussClientAction extends Component {
// legacy format (sent in old emails, shared links, ...)
model = "discuss.channel";
}
const activeThreadLocalId = this.store.Thread.localId({ model, id });
if (activeThreadLocalId !== this.store.discuss.threadLocalId) {
const activeThread = this.store.Thread.get({ model, id });
if (!activeThread || activeThread.notEq(this.store.discuss.thread)) {
const thread =
this.store.Thread.get({ model, id }) ??
(await this.threadService.fetchChannel(parseInt(id)));
@@ -19,8 +19,8 @@
<button
class="o-mail-DiscussSidebar-item btn d-flex align-items-center py-1 px-0 border-0 rounded-0 fw-normal text-reset"
t-att-class="{
'o-bg-inherit': mailbox.localId !== store.discuss.threadLocalId,
'o-active': mailbox.localId === store.discuss.threadLocalId,
'o-bg-inherit': mailbox.notEq(store.discuss.thread),
'o-active': mailbox.eq(store.discuss.thread),
}"
t-on-click="(ev) => this.openThread(ev, mailbox)"
>
@@ -279,15 +279,14 @@ export class MessagingMenu extends Component {
this.store.discuss.activeTab = tabId;
if (
this.store.discuss.activeTab === "mailbox" &&
(!this.store.discuss.threadLocalId ||
this.store.Thread.records[this.store.discuss.threadLocalId].type !== "mailbox")
(!this.store.discuss.thread || this.store.discuss.thread.type !== "mailbox")
) {
this.threadService.setDiscussThread(
Object.values(this.store.Thread.records).find((thread) => thread.id === "inbox")
);
}
if (this.store.discuss.activeTab !== "mailbox") {
this.store.discuss.threadLocalId = null;
delete this.store.discuss.thread;
}
}
@@ -60,13 +60,10 @@ export class DiscussCoreCommon {
...payload,
model: "discuss.channel",
});
this.threadService.remove(thread);
if (thread.localId === this.store.discuss.threadLocalId) {
this.store.discuss.threadLocalId = undefined;
}
this.notificationService.add(_t("You unsubscribed from %s.", thread.displayName), {
type: "info",
});
thread.delete();
});
this.busService.subscribe("discuss.channel/legacy_insert", (payload) => {
this.store.Thread.insert({
@@ -81,8 +81,7 @@ export class DiscussSidebarCategories extends Component {
}
filteredThreads(category) {
return category.threads.filter((threadLocalId) => {
const thread = this.store.Thread.records[threadLocalId];
return category.threads.filter((thread) => {
return (
(thread.is_pinned || thread.group_based_subscription) &&
(!this.state.quickSearchVal ||
@@ -30,13 +30,12 @@
<div t-if="state.editing === category.id" class="p-2" t-ref="selector">
<ChannelSelector category="category" onValidate.bind="stopEditing" autofocus="true"/>
</div>
<t t-foreach="filteredThreads(category)" t-as="threadLocalId" t-key="threadLocalId" t-call="mail.DiscussSidebarChannel">
<t t-set="thread" t-value="store.Thread.records[threadLocalId]"/>
<t t-foreach="filteredThreads(category)" t-as="thread" t-key="thread.localId" t-call="mail.DiscussSidebarChannel">
<t t-set="thread" t-value="thread"/>
</t>
</t>
<t t-elif="category.threads.includes(store.discuss.threadLocalId)" t-call="mail.DiscussSidebarChannel">
<t t-set="threadLocalId" t-value="store.discuss.threadLocalId"/>
<t t-set="thread" t-value="store.Thread.records[threadLocalId]"/>
<t t-elif="category.threads.includes(store.discuss.thread)" t-call="mail.DiscussSidebarChannel">
<t t-set="thread" t-value="store.discuss.thread"/>
</t>
</t>
@@ -44,8 +43,8 @@
<t t-set="counter" t-value="threadService.getCounter(thread)"/>
<button class="o-mail-DiscussSidebarChannel o-mail-DiscussSidebar-item btn btn-secondary d-flex align-items-center w-100 px-0 py-2 border-0 rounded-0 text-reset"
t-att-class="{
'o-bg-inherit': threadLocalId !== store.discuss.threadLocalId,
'o-active': threadLocalId === store.discuss.threadLocalId,
'o-bg-inherit': thread.notEq(store.discuss.thread),
'o-active': thread.eq(store.discuss.thread),
'o-unread': thread.message_unread_counter > 0,
}"
t-on-click="(ev) => this.openThread(ev, thread)"