/** @odoo-module **/ import { attr, clear, link, many, one, Model } from "@mail/model"; Model({ name: "ThreadCache", recordMethods: { async loadMoreMessages() { if (this.isAllHistoryLoaded || this.isLoading) { return; } if (!this.isLoaded) { this.update({ isCacheRefreshRequested: true }); return; } this.update({ isLoadingMore: true }); const messageIds = this.fetchedMessages.map((message) => message.id); const limit = 30; let fetchedMessages; let success; try { fetchedMessages = await this._loadMessages({ limit, maxId: Math.min(...messageIds), }); success = true; } catch { success = false; } if (!this.exists()) { return; } if (success) { if (fetchedMessages.length < limit) { this.update({ isAllHistoryLoaded: true }); } for (const threadView of this.threadViews) { threadView.addComponentHint("more-messages-loaded", { fetchedMessages }); } } this.update({ isLoadingMore: false }); }, /** * @returns {Message[]|undefined} */ async loadNewMessages() { if (this.isLoading) { return; } if (!this.isLoaded) { this.update({ isCacheRefreshRequested: true }); return; } const messageIds = this.fetchedMessages.map((message) => message.id); const fetchedMessages = this._loadMessages({ minId: Math.max(...messageIds, 0) }); if (!fetchedMessages || fetchedMessages.length === 0) { return; } for (const threadView of this.threadViews) { threadView.addComponentHint("new-messages-loaded", { fetchedMessages }); } return fetchedMessages; }, /** * @private * @returns {boolean} */ _computeHasToLoadMessages() { const res = { hasToLoadMessages: false }; if (!this.thread) { // happens during destroy or compute executed in wrong order return res; } if (this.hasLoadingFailed) { return res; } const wasCacheRefreshRequested = this.isCacheRefreshRequested; // mark hint as processed if (this.isCacheRefreshRequested) { res.isCacheRefreshRequested = false; } if (this.thread.isTemporary) { // temporary threads don't exist on the server return res; } if (!wasCacheRefreshRequested && this.threadViews.length === 0) { // don't load message that won't be used return res; } if (this.isLoading) { // avoid duplicate RPC return res; } if (!wasCacheRefreshRequested && this.isLoaded) { // avoid duplicate RPC return res; } if (this.isLoaded) { // Ignore request if it is already loaded or loading. Indeed // messages are automatically sync with server updates already, // so there is never a need to refresh it past the first time. return res; } res.hasToLoadMessages = true; return res; }, /** * @private * @param {Object} [param0={}] * @param {integer} [param0.limit=30] * @param {integer} [param0.maxId] * @param {integer} [param0.minId] * @returns {Message[]} * @throws {Error} when failed to load messages */ async _loadMessages({ limit = 30, maxId, minId } = {}) { this.update({ isLoading: true }); let messages; try { messages = await this.messaging.models["Message"].performRpcMessageFetch( this.thread.fetchMessagesUrl, { ...this.thread.fetchMessagesParams, limit, max_id: maxId, min_id: minId, } ); } catch (e) { if (this.exists()) { this.update({ hasLoadingFailed: true, isLoading: false, }); } throw e; } if (!this.exists()) { return; } this.update({ rawFetchedMessages: link(messages), hasLoadingFailed: false, isLoaded: true, isLoading: false, }); if (!minId && messages.length < limit) { this.update({ isAllHistoryLoaded: true }); } this.messaging.messagingBus.trigger("o-thread-cache-loaded-messages", { fetchedMessages: messages, threadCache: this, }); return messages; }, /** * Split method for `_computeHasToLoadMessages` because it has to write * on 2 fields at once which is not supported by standard compute. * * @private */ _onChangeForHasToLoadMessages() { this.update(this._computeHasToLoadMessages()); }, /** * Loads this thread cache, by fetching the most recent messages in this * conversation. * * @private */ async _onHasToLoadMessagesChanged() { if (!this.hasToLoadMessages) { return; } const fetchedMessages = await this._loadMessages(); if (!this.exists()) { return; } for (const threadView of this.threadViews) { threadView.addComponentHint("messages-loaded", { fetchedMessages }); } this.messaging.messagingBus.trigger("o-thread-loaded-messages", { thread: this.thread, }); }, }, fields: { /** * List of messages that have been fetched by this cache. * * This DOES NOT necessarily includes all messages linked to this thread * cache (@see messages field for that): it just contains list * of successive messages that have been explicitly fetched by this * cache. For all non-main caches, this corresponds to all messages. * For the main cache, however, messages received from the bus * should be displayed on main cache but they have not been explicitly * fetched by cache, so they ARE NOT in this list (at least, not until a * fetch on this thread cache contains this message). * * The distinction between messages and fetched messages is important * to manage "holes" in message list, while still allowing to display * new messages on main cache of thread in real-time. */ fetchedMessages: many("Message", { compute() { if (!this.thread) { return clear(); } return this.rawFetchedMessages.filter((m) => this.thread.messages.includes(m)); }, }), /** * Determines whether the last message fetch failed. */ hasLoadingFailed: attr({ default: false }), /** * Determines whether `this` should load initial messages. * @see `onChangeForHasToLoadMessages` value of this field is mainly set * from this "on change". * @see `isCacheRefreshRequested` to request manual refresh of messages. */ hasToLoadMessages: attr(), isAllHistoryLoaded: attr({ default: false }), isLoaded: attr({ default: false }), isLoading: attr({ default: false }), isLoadingMore: attr({ default: false }), /** * Determines whether `this` should consider refreshing its messages. * This field is a hint that may or may not lead to an actual refresh. * @see `hasToLoadMessages` */ isCacheRefreshRequested: attr({ default: false }), /** * Last message that has been fetched by this thread cache. * * This DOES NOT necessarily mean the last message linked to this thread * cache (@see lastMessage field for that). @see fetchedMessages field * for a deeper explanation about "fetched" messages. */ lastFetchedMessage: one("Message", { compute() { const { length: l, [l - 1]: lastFetchedMessage } = this.orderedFetchedMessages; if (!lastFetchedMessage) { return clear(); } return lastFetchedMessage; }, }), lastMessage: one("Message", { compute() { const { length: l, [l - 1]: lastMessage } = this.orderedMessages; if (!lastMessage) { return clear(); } return lastMessage; }, }), /** * List of messages linked to this cache. */ messages: many("Message", { compute() { if (!this.thread) { return clear(); } let newerMessages; if (!this.lastFetchedMessage) { newerMessages = this.thread.messages; } else { newerMessages = this.thread.messages.filter( (message) => message.id > this.lastFetchedMessage.id ); } return [...this.fetchedMessages, ...this.temporaryMessages, ...newerMessages]; }, }), /** * Ordered list of messages that have been fetched by this cache. * * This DOES NOT necessarily includes all messages linked to this thread * cache (@see orderedMessages field for that). @see fetchedMessages * field for deeper explanation about "fetched" messages. */ orderedFetchedMessages: many("Message", { compute() { return this.fetchedMessages.sort((m1, m2) => (m1.id < m2.id ? -1 : 1)); }, }), /** * Ordered list of messages linked to this cache. */ orderedMessages: many("Message", { compute() { return this.messages.sort((m1, m2) => (m1.id < m2.id ? -1 : 1)); }, }), /** * List of ordered non empty messages linked to this cache. */ orderedNonEmptyMessages: many("Message", { compute() { return this.orderedMessages.filter((message) => !message.isEmpty); }, }), rawFetchedMessages: many("Message"), temporaryMessages: many("Message"), thread: one("Thread", { identifying: true, inverse: "cache" }), /** * States the 'ThreadView' that are currently displaying `this`. */ threadViews: many("ThreadView", { inverse: "threadCache" }), }, onChanges: [ { dependencies: [ "hasLoadingFailed", "isCacheRefreshRequested", "isLoaded", "isLoading", "thread.isTemporary", "threadViews", ], methodName: "_onChangeForHasToLoadMessages", }, { dependencies: ["hasToLoadMessages"], methodName: "_onHasToLoadMessagesChanged", }, ], });